Compare commits
97 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
7fb4b0b145 | ||
|
|
57f60a2fbf | ||
|
|
ec949da416 | ||
|
|
43710b8ada | ||
|
|
247665a846 | ||
|
|
010657c594 | ||
|
|
2d911af97f | ||
|
|
afdfed0160 | ||
|
|
6b86da3716 | ||
|
|
425fac8e9c | ||
|
|
fa2413f937 | ||
|
|
feec057933 | ||
|
|
9127686df7 | ||
|
|
479911ded8 | ||
|
|
69dcd6fadd | ||
|
|
0b7b34b490 | ||
|
|
fff93a3820 | ||
|
|
da21784c73 | ||
|
|
2e03a39b3e | ||
|
|
8e626cdd96 | ||
|
|
472704ad59 | ||
|
|
94c7288016 | ||
|
|
957ff98cec | ||
|
|
80178f66c3 | ||
|
|
37324cbcfc | ||
|
|
9dd362a8a4 | ||
|
|
90605a2067 | ||
|
|
18cfddbbe7 | ||
|
|
17e671200e | ||
|
|
bb94342cc8 | ||
|
|
b86cf8b99f | ||
|
|
f472c06cbf | ||
|
|
c58834309b | ||
|
|
75fdd3e17f | ||
|
|
568c5eccda | ||
|
|
363f226a1c | ||
|
|
bf447b1ada | ||
|
|
90cb05bd2d | ||
|
|
a98d6d9ce1 | ||
|
|
f83e39d734 | ||
|
|
52c1909229 | ||
|
|
f0d893c689 | ||
|
|
c8339c4be1 | ||
|
|
954d697b5f | ||
|
|
ace2bb61ef |
14
.github/workflows/build_and_push.yml
vendored
14
.github/workflows/build_and_push.yml
vendored
@@ -11,10 +11,10 @@ jobs:
|
||||
build_n_push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: setup-node
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
@@ -27,26 +27,26 @@ jobs:
|
||||
run: CI=false npm run build
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: wiretrustee/dashboard
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
-
|
||||
name: Docker build and push
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
|
||||
37
README.md
37
README.md
@@ -36,24 +36,43 @@ Disclaimer. We believe that proper user management system is not a trivial task
|
||||
use Auth0 service that covers all our needs (user management, social login, JTW for the management API).
|
||||
Auth0 so far is the only 3rd party dependency that can't be really self-hosted.
|
||||
|
||||
1. install [Docker](https://docs.docker.com/get-docker/)
|
||||
2. register [Auth0](https://auth0.com/) account
|
||||
3. running Wiretrustee UI Dashboard requires the following Auth0 environmental variables to be set (see docker command below):
|
||||
1. Install [Docker](https://docs.docker.com/get-docker/)
|
||||
2. Register [Auth0](https://auth0.com/) account
|
||||
3. Running Wiretrustee UI Dashboard requires the following Auth0 environmental variables to be set (see docker command below):
|
||||
|
||||
```AUTH0_DOMAIN``` ```AUTH0_CLIENT_ID``` ```AUTH0_AUDIENCE```
|
||||
`AUTH0_DOMAIN` `AUTH0_CLIENT_ID` `AUTH0_AUDIENCE`
|
||||
|
||||
To obtain these, please use [Auth0 React SDK Guide](https://auth0.com/docs/quickstart/spa/react/01-login#configure-auth0) up until "Configure Allowed Web Origins"
|
||||
|
||||
4. Wiretrustee UI Dashboard uses Wiretrustee Management Service HTTP API, so setting ```WIRETRUSTEE_MGMT_API_ENDPOINT``` is required. Most likely it will be ```http://localhost:33071``` if you are hosting Management API on the same server.
|
||||
4. Wiretrustee UI Dashboard uses Wiretrustee Management Service HTTP API, so setting `NETBIRD_MGMT_API_ENDPOINT` is required. Most likely it will be `http://localhost:33071` if you are hosting Management API on the same server.
|
||||
5. Run docker container without SSL (Let's Encrypt):
|
||||
|
||||
```docker run -d --name wiretrustee-dashboard --rm -p 80:80 -p 443:443 -e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> -e AUTH0_CLIENT_ID=<SET YOUR CLIENT ID> -e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> -e WIRETRUSTEE_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> wiretrustee/dashboard:main```
|
||||
```shell
|
||||
docker run -d --name wiretrustee-dashboard \
|
||||
--rm -p 80:80 -p 443:443 \
|
||||
-e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> \
|
||||
-e AUTH0_CLIENT_ID=<SET YOUR CLIENT ID> \
|
||||
-e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> \
|
||||
-e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> \
|
||||
wiretrustee/dashboard:main
|
||||
```
|
||||
6. Run docker container with SSL (Let's Encrypt):
|
||||
|
||||
```docker run -d --name wiretrustee-dashboard --rm -p 80:80 -p 443:443 -e NGINX_SSL_PORT=443 -e LETSENCRYPT_DOMAIN=<YOUR PUBLIC DOMAIN> -e LETSENCRYPT_EMAIL=<YOUR EMAIL> -e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> -e AUTH0_CLIENT_ID=<SET YOUR CLEITN ID> -e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> -e WIRETRUSTEE_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> wiretrustee/dashboard:main```
|
||||
```shell
|
||||
docker run -d --name wiretrustee-dashboard \
|
||||
--rm -p 80:80 -p 443:443 \
|
||||
-e NGINX_SSL_PORT=443 \
|
||||
-e LETSENCRYPT_DOMAIN=<YOUR PUBLIC DOMAIN> \
|
||||
-e LETSENCRYPT_EMAIL=<YOUR EMAIL> \
|
||||
-e AUTH0_DOMAIN=<SET YOUR AUTH DOMAIN> \
|
||||
-e AUTH0_CLIENT_ID=<SET YOUR CLEITN ID> \
|
||||
-e AUTH0_AUDIENCE=<SET YOUR AUDIENCE> \
|
||||
-e NETBIRD_MGMT_API_ENDPOINT=<SET YOUR MANAGEMETN API URL> \
|
||||
wiretrustee/dashboard:main
|
||||
```
|
||||
|
||||
## How to run local development
|
||||
1. Install node 16
|
||||
2. create and update the src/.local-config.json file. This file should contain values to be replaced from src/config.json
|
||||
2. create and update the `src/.local-config.json` file. This file should contain values to be replaced from `src/config.json`
|
||||
3. run `npm install`
|
||||
4. run `npm run start dev`
|
||||
4. run `npm run start dev`
|
||||
|
||||
@@ -44,19 +44,21 @@ fi
|
||||
export AUTH_AUTHORITY=${AUTH_AUTHORITY:-https://$AUTH0_DOMAIN}
|
||||
export AUTH_CLIENT_ID=${AUTH_CLIENT_ID:-$AUTH0_CLIENT_ID}
|
||||
export AUTH_AUDIENCE=${AUTH_AUDIENCE:-$AUTH0_AUDIENCE}
|
||||
export AUTH_REDIRECT_URI=${AUTH_REDIRECT_URI}
|
||||
export AUTH_SILENT_REDIRECT_URI=${AUTH_SILENT_REDIRECT_URI}
|
||||
export USE_AUTH0=${USE_AUTH0:-true}
|
||||
export AUTH_SUPPORTED_SCOPES=${AUTH_SUPPORTED_SCOPES:-openid profile email api offline_access email_verified}
|
||||
|
||||
export NETBIRD_MGMT_API_ENDPOINT=$(echo $NETBIRD_MGMT_API_ENDPOINT | sed -E 's/(:80|:443)$//')
|
||||
export NETBIRD_MGMT_GRPC_API_ENDPOINT=${NETBIRD_MGMT_GRPC_API_ENDPOINT}
|
||||
export NETBIRD_HOTJAR_TRACK_ID=${NETBIRD_HOTJAR_TRACK_ID}
|
||||
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"
|
||||
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE"
|
||||
|
||||
MAIN_JS=$(find /usr/share/nginx/html/static/js/main.*js)
|
||||
OIDC_TRUSTED_DOMAINS="/usr/share/nginx/html/OidcTrustedDomains.js"
|
||||
cp "$MAIN_JS" "$MAIN_JS".copy
|
||||
|
||||
8033
package-lock.json
generated
8033
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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,11 +27,15 @@
|
||||
"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",
|
||||
@@ -42,6 +46,7 @@
|
||||
"redux-saga": "^1.1.3",
|
||||
"styled-components": "^5.3.5",
|
||||
"tailwindcss": "^3.0.23",
|
||||
"ts-md5": "^1.3.1",
|
||||
"typesafe-actions": "^5.1.0",
|
||||
"typescript": "^4.6.4",
|
||||
"web-vitals": "^2.1.4"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="robots" content="noindex">
|
||||
<meta
|
||||
name="description"
|
||||
content="NetBird Management Dashboard"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
Disallow: /
|
||||
|
||||
161
src/App.tsx
161
src/App.tsx
@@ -1,23 +1,41 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {Provider} from "react-redux";
|
||||
import {Redirect, Route, Switch} from 'react-router-dom';
|
||||
import Navbar from './components/Navbar';
|
||||
import Peers from './views/Peers';
|
||||
import FooterComponent from './components/FooterComponent';
|
||||
import SetupKeys from "./views/SetupKeys";
|
||||
import AddPeer from "./views/AddPeer";
|
||||
import Users from './views/Users';
|
||||
import AccessControl from './views/AccessControl';
|
||||
import Routes from './views/Routes';
|
||||
import {apiClient, store} from "./store";
|
||||
import {hotjar} from 'react-hotjar';
|
||||
import {getConfig} from "./config";
|
||||
import Banner from "./components/Banner";
|
||||
import {store} from "./store";
|
||||
import { Col, Layout, Row} from 'antd';
|
||||
import {Col, ConfigProvider, Layout, Row} from "antd";
|
||||
import {Container} from "./components/Container";
|
||||
import {withOidcSecure} from '@axa-fr/react-oidc';
|
||||
import Navbar from "./components/Navbar";
|
||||
import {Redirect, Route, Switch} from "react-router-dom";
|
||||
import {withOidcSecure} from "@axa-fr/react-oidc";
|
||||
import Peers from "./views/Peers";
|
||||
import Routes from "./views/Routes";
|
||||
import AddPeer from "./views/AddPeer";
|
||||
import SetupKeys from "./views/SetupKeys";
|
||||
import AccessControl from "./views/AccessControl";
|
||||
import Users from "./views/Users";
|
||||
import FooterComponent from "./components/FooterComponent";
|
||||
import {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;
|
||||
|
||||
function App() {
|
||||
const run = useRef(false)
|
||||
const [show, setShow] = useState(false)
|
||||
const {hotjarTrackID,tokenSource} = getConfig();
|
||||
useTokenSource(tokenSource)
|
||||
const {getTokenSilently} = useGetTokenSilently();
|
||||
// @ts-ignore
|
||||
if (hotjarTrackID && window._DATADOG_SYNTHETICS_BROWSER === undefined) {
|
||||
hotjar.initialize(hotjarTrackID, 6);
|
||||
}
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
@@ -25,7 +43,6 @@ function App() {
|
||||
const hideMenu = () => {
|
||||
if (window.innerWidth > 768 && isOpen) {
|
||||
setIsOpen(false);
|
||||
console.log('i resized');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -34,49 +51,83 @@ function App() {
|
||||
return () => {
|
||||
window.removeEventListener('resize', hideMenu);
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!run.current) {
|
||||
run.current = true
|
||||
apiClient.request<User[]>('GET', `/api/users`, {getAccessTokenSilently: getTokenSilently})
|
||||
.then(() => {
|
||||
setShow(true)
|
||||
})
|
||||
.catch(e => {
|
||||
setShow(true)
|
||||
console.log(e)
|
||||
})
|
||||
}
|
||||
|
||||
}, [getTokenSilently])
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Layout>
|
||||
<Banner/>
|
||||
<Header className="header" style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-around",
|
||||
alignContent: "center"
|
||||
}}>
|
||||
<Row justify="space-around" align="middle">
|
||||
<Col span={24}>
|
||||
<Container>
|
||||
<Navbar/>
|
||||
</Container>
|
||||
</Col>
|
||||
</Row>
|
||||
</Header>
|
||||
<Content style={{ minHeight: "100vh"}}>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
render={() => {
|
||||
return (
|
||||
<Redirect to="/peers"/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Route path='/peers' exact component={withOidcSecure(Peers)}/>
|
||||
<Route path="/add-peer" component={withOidcSecure(AddPeer)}/>
|
||||
<Route path="/setup-keys" component={withOidcSecure(SetupKeys)}/>
|
||||
<Route path="/acls" component={withOidcSecure(AccessControl)}/>
|
||||
<Route path="/routes" component={withOidcSecure(Routes)}/>
|
||||
<Route path="/users" component={withOidcSecure(Users)}/>
|
||||
</Switch>
|
||||
</Content>
|
||||
<FooterComponent/>
|
||||
</Layout>
|
||||
</Provider>
|
||||
);
|
||||
<>
|
||||
<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="/setup-keys" component={withOidcSecure(SetupKeys)}/>
|
||||
<Route path="/acls" component={withOidcSecure(AccessControl)}/>
|
||||
<Route path="/routes" component={withOidcSecure(Routes)}/>
|
||||
<Route path="/users" component={withOidcSecure(Users)}/>
|
||||
<Route path="/dns" component={withOidcSecure(DNS)}/>
|
||||
<Route path="/activity" component={withOidcSecure(Activity)}/>
|
||||
<Route path="/settings" component={withOidcSecure(Settings)}/>
|
||||
</Switch>
|
||||
</Content>
|
||||
<FooterComponent/>
|
||||
</Layout>
|
||||
}
|
||||
</Provider>
|
||||
|
||||
</ConfigProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -16,7 +16,7 @@ const AccessControlModalGroups:React.FC<Props> = ({data, title, visible, onCance
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal title={title} visible={isModalVisible} onCancel={() => onCancel()} footer={null}>
|
||||
<Modal title={title} open={isModalVisible} onCancel={() => onCancel()} footer={null}>
|
||||
<List
|
||||
itemLayout="horizontal"
|
||||
dataSource={data as Group[] | undefined}
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import { actions as ruleActions } from '../store/rule';
|
||||
import {actions as ruleActions} from '../store/rule';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Row,
|
||||
Typography,
|
||||
Divider,
|
||||
Drawer,
|
||||
Form,
|
||||
Input,
|
||||
Radio,
|
||||
RadioChangeEvent,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Button, Drawer, Form, Divider, Select, Tag, Radio, RadioChangeEvent
|
||||
Tag,
|
||||
Typography
|
||||
} from "antd";
|
||||
import {ArrowRightOutlined, CheckOutlined, CloseOutlined, FlagFilled, QuestionCircleFilled} from "@ant-design/icons";
|
||||
import type { CustomTagProps } from 'rc-select/lib/BaseSelect'
|
||||
import {CloseOutlined, FlagFilled, QuestionCircleFilled} from "@ant-design/icons";
|
||||
import type {CustomTagProps} from 'rc-select/lib/BaseSelect'
|
||||
import {Rule, RuleToSave} from "../store/rule/types";
|
||||
import { uniq } from "lodash"
|
||||
import {uniq} from "lodash"
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import {useOidcAccessToken} from "@axa-fr/react-oidc";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
const {Paragraph} = Typography;
|
||||
const {Option} = Select;
|
||||
|
||||
interface FormRule extends Rule {
|
||||
tagSourceGroups: string[]
|
||||
@@ -28,11 +34,11 @@ interface FormRule extends Rule {
|
||||
}
|
||||
|
||||
const AccessControlNew = () => {
|
||||
const {accessToken} = useOidcAccessToken()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const setupNewRuleVisible = useSelector((state: RootState) => state.rule.setupNewRuleVisible)
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const rule = useSelector((state: RootState) => state.rule.rule)
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const rule = useSelector((state: RootState) => state.rule.rule)
|
||||
const savedRule = useSelector((state: RootState) => state.rule.savedRule)
|
||||
|
||||
const [editName, setEditName] = useState(false)
|
||||
@@ -72,7 +78,7 @@ const AccessControlNew = () => {
|
||||
setTagGroups(groups?.map(g => g.name) || [])
|
||||
}, [groups])
|
||||
|
||||
const createRuleToSave = ():RuleToSave => {
|
||||
const createRuleToSave = (): RuleToSave => {
|
||||
const sources = groups?.filter(g => formRule.tagSourceGroups.includes(g.name)).map(g => g.id || '') || []
|
||||
const destinations = groups?.filter(g => formRule.tagDestinationGroups.includes(g.name)).map(g => g.id || '') || []
|
||||
const sourcesNoId = formRule.tagSourceGroups.filter(s => !tagGroups.includes(s))
|
||||
@@ -96,14 +102,17 @@ const AccessControlNew = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
const ruleToSave = createRuleToSave()
|
||||
dispatch(ruleActions.saveRule.request({getAccessTokenSilently:accessToken, payload: ruleToSave}))
|
||||
dispatch(ruleActions.saveRule.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: ruleToSave
|
||||
}))
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log('errorInfo', errorInfo)
|
||||
});
|
||||
};
|
||||
|
||||
const setVisibleNewRule = (status:boolean) => {
|
||||
const setVisibleNewRule = (status: boolean) => {
|
||||
dispatch(ruleActions.setSetupNewRuleVisible(status));
|
||||
}
|
||||
|
||||
@@ -121,7 +130,7 @@ const AccessControlNew = () => {
|
||||
setVisibleNewRule(false)
|
||||
}
|
||||
|
||||
const onChange = (data:any) => {
|
||||
const onChange = (data: any) => {
|
||||
setFormRule({...formRule, ...data})
|
||||
}
|
||||
|
||||
@@ -139,7 +148,7 @@ const AccessControlNew = () => {
|
||||
})
|
||||
};
|
||||
|
||||
const handleChangeDisabled = ({ target: { value } }: RadioChangeEvent) => {
|
||||
const handleChangeDisabled = ({target: {value}}: RadioChangeEvent) => {
|
||||
setFormRule({
|
||||
...formRule,
|
||||
disabled: value
|
||||
@@ -147,7 +156,7 @@ const AccessControlNew = () => {
|
||||
};
|
||||
|
||||
const tagRender = (props: CustomTagProps) => {
|
||||
const { label, value, closable, onClose } = props;
|
||||
const {label, value, closable, onClose} = props;
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -159,7 +168,7 @@ const AccessControlNew = () => {
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{ marginRight: 3 }}
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{value}</strong>
|
||||
</Tag>
|
||||
@@ -169,12 +178,12 @@ const AccessControlNew = () => {
|
||||
const optionRender = (label: string) => {
|
||||
let peersCount = ''
|
||||
const g = groups.find(_g => _g.name === label)
|
||||
if (g) peersCount = ` - ${g.peers_count || 0} ${(!g.peers_count || parseInt(g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
if (g) peersCount = ` - ${g.peers_count || 0} ${(!g.peers_count || parseInt(g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{ marginRight: 3 }}
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{label}</strong>
|
||||
</Tag>
|
||||
@@ -186,25 +195,27 @@ const AccessControlNew = () => {
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{ margin: '8px 0' }} />
|
||||
<Divider style={{margin: '8px 0'}}/>
|
||||
<Row style={{padding: '0 8px 4px'}}>
|
||||
<Col flex="auto">
|
||||
<span style={{color: "#9CA3AF"}}>Add new group by pressing "Enter"</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z" fill="#9CA3AF"/>
|
||||
<path
|
||||
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
|
||||
fill="#9CA3AF"/>
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
|
||||
const toggleEditName = (status:boolean) => {
|
||||
const toggleEditName = (status: boolean) => {
|
||||
setEditName(status);
|
||||
}
|
||||
|
||||
const toggleEditDescription = (status:boolean) => {
|
||||
const toggleEditDescription = (status: boolean) => {
|
||||
setEditDescription(status);
|
||||
}
|
||||
|
||||
@@ -217,10 +228,10 @@ const AccessControlNew = () => {
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = []
|
||||
if (!value.length) {
|
||||
return Promise.reject(new Error("Please enter ate least one group"))
|
||||
return Promise.reject(new Error("Please enter at least one group"))
|
||||
}
|
||||
|
||||
value.forEach(function(v: string) {
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v)
|
||||
}
|
||||
@@ -248,7 +259,8 @@ const AccessControlNew = () => {
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button onClick={onCancel} disabled={savedRule.loading}>Cancel</Button>
|
||||
<Button type="primary" disabled={savedRule.loading} onClick={handleFormSubmit}>{`${formRule.id ? 'Save' : 'Create'}`}</Button>
|
||||
<Button type="primary" disabled={savedRule.loading}
|
||||
onClick={handleFormSubmit}>{`${formRule.id ? 'Save' : 'Create'}`}</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
@@ -258,37 +270,51 @@ const AccessControlNew = () => {
|
||||
<Header style={{margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px"}}>
|
||||
<Row align="top">
|
||||
<Col flex="none" style={{display: "flex"}}>
|
||||
{!editName && !editDescription && formRule.id &&
|
||||
{!editName && !editDescription && formRule.id &&
|
||||
<button type="button" aria-label="Close" className="ant-drawer-close"
|
||||
style={{paddingTop: 3}}
|
||||
onClick={onCancel}>
|
||||
<span role="img" aria-label="close" className="anticon anticon-close">
|
||||
<span role="img" aria-label="close"
|
||||
className="anticon anticon-close">
|
||||
<CloseOutlined size={16}/>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
{ !editName && formRule.id ? (
|
||||
<div className={"access-control input-text ant-drawer-title"} onClick={() => toggleEditName(true)}>{formRule.id ? formRule.name : 'New Rule'}</div>
|
||||
{!editName && formRule.id ? (
|
||||
<div className={"access-control input-text ant-drawer-title"}
|
||||
onClick={() => toggleEditName(true)}>{formRule.id ? formRule.name : 'New Rule'}</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Name"
|
||||
rules={[{required: true, message: 'Please add a name for this access rule', whitespace: true}]}
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please add a name for this access rule',
|
||||
whitespace: true
|
||||
}]}
|
||||
>
|
||||
<Input placeholder="Add rule name..." ref={inputNameRef} onPressEnter={() => toggleEditName(false)} onBlur={() => toggleEditName(false)} autoComplete="off"/>
|
||||
<Input placeholder="Add rule name..." ref={inputNameRef}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)} autoComplete="off"/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{ !editDescription ? (
|
||||
<div className={"access-control input-text ant-drawer-subtitle"} onClick={() => toggleEditDescription(true)}>{formRule.description && formRule.description.trim() !== "" ? formRule.description : 'Add description...'}</div>
|
||||
{!editDescription ? (
|
||||
<div className={"access-control input-text ant-drawer-subtitle"}
|
||||
onClick={() => toggleEditDescription(true)}>
|
||||
{formRule.description && formRule.description.trim() !== "" ? formRule.description : 'Add description...'}
|
||||
</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="Description"
|
||||
style={{marginTop: 24}}
|
||||
>
|
||||
<Input placeholder="Add description..." ref={inputDescriptionRef} onPressEnter={() => toggleEditDescription(false)} onBlur={() => toggleEditDescription(false)} autoComplete="off"/>
|
||||
<Input placeholder="Add description..." ref={inputDescriptionRef}
|
||||
onPressEnter={() => toggleEditDescription(false)}
|
||||
onBlur={() => toggleEditDescription(false)}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Col>
|
||||
@@ -322,10 +348,10 @@ const AccessControlNew = () => {
|
||||
<Form.Item
|
||||
name="tagSourceGroups"
|
||||
label="Source groups"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
rules={[{validator: selectValidator}]}
|
||||
>
|
||||
<Select mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
style={{width: '100%'}}
|
||||
placeholder="Tags Mode"
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangeSource}
|
||||
@@ -343,10 +369,10 @@ const AccessControlNew = () => {
|
||||
<Form.Item
|
||||
name="tagDestinationGroups"
|
||||
label="Destination groups"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
rules={[{validator: selectValidator}]}
|
||||
>
|
||||
<Select
|
||||
mode="tags" style={{ width: '100%' }}
|
||||
mode="tags" style={{width: '100%'}}
|
||||
placeholder="Tags Mode"
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangeDestination}
|
||||
@@ -367,10 +393,14 @@ const AccessControlNew = () => {
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
<Paragraph>
|
||||
At the moment access rules are bi-directional by default, this means both source and destination can talk to each-other in both directions. However destination peers will not be able to communicate with each other, nor will the source peers.
|
||||
At the moment access rules are bi-directional by default, this means both
|
||||
source and destination can talk to each-other in both directions. However
|
||||
destination peers will not be able to communicate with each other, nor will
|
||||
the source peers.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
If you want to enable all peers of the same group to talk to each other - you can add that group both as a receiver and as a destination.
|
||||
If you want to enable all peers of the same group to talk to each other -
|
||||
you can add that group both as a receiver and as a destination.
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -378,8 +408,7 @@ const AccessControlNew = () => {
|
||||
<Col span={24}>
|
||||
<Divider></Divider>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
|
||||
href="https://docs.netbird.io/docs/overview/acls" style={{color: 'rgb(07, 114, 128)'}}>Learn
|
||||
more about access controls</Button>
|
||||
href="https://docs.netbird.io/docs/overview/acls">Learn more about access controls</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import { useState } from "react";
|
||||
import {useEffect, useState} from "react";
|
||||
import {Button, Col, Row, Space, Typography} from "antd";
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
import {Md5} from "ts-md5";
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
const Banner = () => {
|
||||
const [show, setShow] = useState(true);
|
||||
const [show, setShow] = useState(false);
|
||||
const banner_md5_key = 'banner_md5'
|
||||
const banner_closed_key = 'banner_closed'
|
||||
|
||||
const dismiss = () => {
|
||||
setShow(false);
|
||||
localStorage.setItem(banner_closed_key,'true');
|
||||
};
|
||||
|
||||
const announcement = "New Release! Manage DNS with NetBird."
|
||||
|
||||
const announcement_md5 = Md5.hashStr(announcement)
|
||||
|
||||
const linkLearnMore = () => {
|
||||
return (
|
||||
<a
|
||||
href="https://netbird.io/docs/how-to-guides/network-routes"
|
||||
href="https://netbird.io/docs/how-to-guides/nameservers"
|
||||
className="font-bold underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
@@ -22,12 +30,24 @@ const Banner = () => {
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
let store_banner_md5 = localStorage.getItem(banner_md5_key);
|
||||
let stored_banner_closed = localStorage.getItem(banner_closed_key);
|
||||
|
||||
if((!stored_banner_closed || stored_banner_closed !== 'true') ||
|
||||
(!store_banner_md5 || store_banner_md5 !== announcement_md5)) {
|
||||
//setShow(true);
|
||||
localStorage.setItem(banner_md5_key,announcement_md5);
|
||||
localStorage.setItem(banner_closed_key,'false');
|
||||
}
|
||||
},[])
|
||||
|
||||
return show ? (
|
||||
<div className="relative bg-indigo-600 white" color="white" style={{position: "relative", padding: "0.3rem"}} >
|
||||
<Row>
|
||||
<Col xs={24} sm={0} lg={0}>
|
||||
<Text className="ant-col-md-0" style={{color: "#ffffff"}}>
|
||||
New Release! Access private networks with the Network Routes feature.
|
||||
{announcement}
|
||||
</Text>
|
||||
</Col>
|
||||
<Col xs={24} sm={0} lg={0}>
|
||||
@@ -38,7 +58,7 @@ const Banner = () => {
|
||||
<Col xs={0} sm={24}>
|
||||
<Space align="center" style={{display: "flex", justifyContent: "center"}}>
|
||||
<Text style={{color: "#ffffff"}}>
|
||||
New Release! Access private networks with the Network Routes feature.
|
||||
{announcement}
|
||||
</Text>
|
||||
<span>
|
||||
{linkLearnMore()}
|
||||
|
||||
@@ -1,24 +1,36 @@
|
||||
import {copyToClipboard} from "../utils/common";
|
||||
import {Button, message} from "antd";
|
||||
import {StepCommand} from "./addpeer/types";
|
||||
import React from "react";
|
||||
import {Button, message, Typography} from "antd";
|
||||
import React, {ReactNode} from "react";
|
||||
|
||||
const {Text} = Typography;
|
||||
|
||||
type Props = {
|
||||
keyMessage: string;
|
||||
text: string;
|
||||
toCopy: string;
|
||||
body: ReactNode;
|
||||
messageText: string;
|
||||
styleNotification?: any;
|
||||
style?: any;
|
||||
className?:any;
|
||||
className?: any;
|
||||
};
|
||||
|
||||
const ButtonCopyMessage:React.FC<Props> = ({ keyMessage, text, messageText, styleNotification, style, className}) => {
|
||||
const ButtonCopyMessage: React.FC<Props> = ({
|
||||
keyMessage,
|
||||
toCopy,
|
||||
body,
|
||||
messageText,
|
||||
styleNotification,
|
||||
style,
|
||||
className
|
||||
}) => {
|
||||
const copyTextMessage = () => {
|
||||
copyToClipboard(text)
|
||||
message.success({ content: `${messageText}`, key: keyMessage, duration: 1, style: (styleNotification || {}) });
|
||||
copyToClipboard(toCopy)
|
||||
message.success({content: `${messageText}`, key: keyMessage, duration: 1, style: (styleNotification || {})});
|
||||
}
|
||||
return (
|
||||
<Button type="text" onClick={copyTextMessage} style={style || {}} className={className}>{text}</Button>
|
||||
<Button type="text" onClick={copyTextMessage} style={style || {}} className={className}>
|
||||
{body}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
import React from "react";
|
||||
import loading from "../assets/bars.svg";
|
||||
import {Space} from "antd";
|
||||
import {OidcSecure} from "@axa-fr/react-oidc";
|
||||
|
||||
type Props = {
|
||||
type LoadingProps = {
|
||||
padding?: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
const Loading:React.FC<Props> = ({padding, width, height}) => (
|
||||
<Space direction="vertical" align="center" style={{display: 'flex', padding: `${padding || `.25em`}`}}>
|
||||
<img src={loading} alt="Loading" style={{width: `${width || '25px'}`, height: `${height || '25px'}`}}/>
|
||||
const Loading: React.FC<LoadingProps> = ({padding, width, height}) => (
|
||||
<Space direction="vertical" align="center" style={{
|
||||
marginTop: `-${height ? (height / 2) + "px" : '-25px'}`,
|
||||
marginLeft: `-${width ? (width / 2) + "px" : '-25px'}`,
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
display: 'flex'
|
||||
}}>
|
||||
<img src={loading} alt="Loading"
|
||||
style={{width: `${width ? width + "px" : '25px'}`, height: `${height ? height + "px" : '25px'}`}}/>
|
||||
</Space>
|
||||
);
|
||||
|
||||
export default Loading;
|
||||
// Wrapper of Loading to handle cases when it is shown within the authenticated layout and has to trigger authentication when token expires.
|
||||
export const SecureLoading = (props: LoadingProps) => (
|
||||
<OidcSecure>
|
||||
<Loading {...props} />
|
||||
</OidcSecure>
|
||||
);
|
||||
|
||||
@@ -1,35 +1,57 @@
|
||||
import {OidcUserStatus, useOidc, useOidcUser} from "@axa-fr/react-oidc";
|
||||
import {Button, Result} from "antd";
|
||||
import {useOidc, useOidcUser} from "@axa-fr/react-oidc";
|
||||
import {Anchor, Button, Col, Result, Row, Space} from "antd";
|
||||
import React from "react";
|
||||
import {getConfig} from "../config";
|
||||
import {ResultStatusType} from "antd/lib/result";
|
||||
|
||||
const {Link} = Anchor;
|
||||
|
||||
function LoginError() {
|
||||
const { logout } = useOidc();
|
||||
const {logout} = useOidc();
|
||||
const config = getConfig();
|
||||
const { oidcUserLoadingState } = useOidcUser();
|
||||
const {oidcUserLoadingState} = useOidcUser();
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
|
||||
if (urlParams.get("error") === "access_denied") {
|
||||
|
||||
let title = urlParams.get("error_description")
|
||||
let status: ResultStatusType = "warning"
|
||||
// this comes from the auth0 rule that links accounts
|
||||
if (title === "account linked successfully") {
|
||||
status = "success"
|
||||
title = "Your account has been linked successfully. Please log in again to complete the setup."
|
||||
}
|
||||
|
||||
return <Result
|
||||
status="warning"
|
||||
title={urlParams.get("error_description")}
|
||||
status={status}
|
||||
title={title}
|
||||
extra={<>
|
||||
<a href={window.location.origin}>
|
||||
<Button type="primary">
|
||||
Try again
|
||||
</Button>
|
||||
</a>
|
||||
<Button type="primary" onClick={function () {
|
||||
logout("",{client_id:config.clientId})
|
||||
<Space style={{
|
||||
display: "flex-inline",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-around",
|
||||
alignContent: "center"
|
||||
}}>
|
||||
Log out
|
||||
<h4>Already verified your email address?</h4>
|
||||
<a href={window.location.origin}>
|
||||
<Button type="primary">
|
||||
Continue
|
||||
</Button>
|
||||
</a>
|
||||
|
||||
<Button type="link" onClick={function () {
|
||||
logout("", {client_id: config.clientId})
|
||||
}}>
|
||||
Trouble logging in? Try again.
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
</>
|
||||
}
|
||||
/>
|
||||
}
|
||||
return <div>{"Login Error: User state: "+oidcUserLoadingState}</div>
|
||||
return <div>{"Login Error: User state: " + oidcUserLoadingState}</div>
|
||||
}
|
||||
|
||||
export default LoginError;
|
||||
662
src/components/NameServerGroupUpdate.tsx
Normal file
662
src/components/NameServerGroupUpdate.tsx
Normal file
@@ -0,0 +1,662 @@
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as nsGroupActions} from '../store/nameservers';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Drawer,
|
||||
Form,
|
||||
FormListFieldData,
|
||||
Input,
|
||||
InputNumber,
|
||||
message,
|
||||
Radio,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from "antd";
|
||||
import {
|
||||
CloseOutlined,
|
||||
FlagFilled,
|
||||
MinusCircleOutlined,
|
||||
PlusOutlined,
|
||||
QuestionCircleFilled,
|
||||
QuestionCircleOutlined
|
||||
} from "@ant-design/icons";
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import cidrRegex from 'cidr-regex';
|
||||
import {NameServer, NameServerGroup, NameServerGroupToSave} from "../store/nameservers/types";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups"
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
|
||||
interface formNSGroup extends NameServerGroup {
|
||||
}
|
||||
|
||||
const NameServerGroupUpdate = () => {
|
||||
const {
|
||||
tagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidator
|
||||
} = useGetGroupTagHelpers()
|
||||
const dispatch = useDispatch()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const {Option} = Select;
|
||||
const nsGroup = useSelector((state: RootState) => state.nameserverGroup.nameserverGroup)
|
||||
const setupNewNameServerGroupVisible = useSelector((state: RootState) => state.nameserverGroup.setupNewNameServerGroupVisible)
|
||||
const savedNSGroup = useSelector((state: RootState) => state.nameserverGroup.savedNameServerGroup)
|
||||
const nsGroupData = useSelector((state: RootState) => state.nameserverGroup.data);
|
||||
|
||||
const [formNSGroup, setFormNSGroup] = useState({} as formNSGroup)
|
||||
const [form] = Form.useForm()
|
||||
const [editName, setEditName] = useState(false)
|
||||
const [isPrimary, setIsPrimary] = useState(false)
|
||||
const [editDescription, setEditDescription] = useState(false)
|
||||
const inputNameRef = useRef<any>(null)
|
||||
const inputDescriptionRef = useRef<any>(null)
|
||||
const [selectCustom, setSelectCustom] = useState(false)
|
||||
|
||||
const optionsDisabledEnabled = [{label: 'Enabled', value: true}, {label: 'Disabled', value: false}]
|
||||
const optionsPrimary = [{label: 'Yes', value: true}, {label: 'No', value: false}]
|
||||
|
||||
useEffect(() => {
|
||||
if (editName) inputNameRef.current!.focus({
|
||||
cursor: 'end',
|
||||
});
|
||||
}, [editName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editDescription) inputDescriptionRef.current!.focus({
|
||||
cursor: 'end',
|
||||
});
|
||||
}, [editDescription]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!nsGroup) return
|
||||
|
||||
let newFormGroup = {
|
||||
...nsGroup,
|
||||
groups: getGroupNamesFromIDs(nsGroup.groups),
|
||||
} as formNSGroup
|
||||
setFormNSGroup(newFormGroup)
|
||||
form.setFieldsValue(newFormGroup)
|
||||
if (nsGroup.id) {
|
||||
setSelectCustom(true)
|
||||
}
|
||||
if (nsGroup.primary !== undefined) {
|
||||
setIsPrimary(nsGroup.primary)
|
||||
}
|
||||
}, [nsGroup])
|
||||
|
||||
const onCancel = () => {
|
||||
dispatch(nsGroupActions.setSetupNewNameServerGroupVisible(false));
|
||||
dispatch(nsGroupActions.setNameServerGroup(
|
||||
{
|
||||
id: '',
|
||||
name: '',
|
||||
description: '',
|
||||
primary: false,
|
||||
domains: [],
|
||||
nameservers: [] as NameServer[],
|
||||
groups: [],
|
||||
enabled: false,
|
||||
} as NameServerGroup
|
||||
))
|
||||
setEditName(false)
|
||||
setSelectCustom(false)
|
||||
setIsPrimary(false)
|
||||
}
|
||||
|
||||
const onChange = (changedValues: any) => {
|
||||
if (changedValues.primary !== undefined) {
|
||||
setIsPrimary(changedValues.primary)
|
||||
}
|
||||
setFormNSGroup({...formNSGroup, ...changedValues})
|
||||
}
|
||||
|
||||
let googleChoice = 'Google DNS'
|
||||
let cloudflareChoice = 'Cloudflare DNS'
|
||||
let quad9Choice = 'Quad9 DNS'
|
||||
let customChoice = 'Add custom nameserver'
|
||||
|
||||
let defaultDNSOptions: NameServerGroup[] = [
|
||||
{
|
||||
name: googleChoice,
|
||||
description: 'Google DNS servers',
|
||||
domains: [],
|
||||
primary: true,
|
||||
nameservers: [
|
||||
{
|
||||
ip: "8.8.8.8",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
{
|
||||
ip: "8.8.4.4",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
],
|
||||
groups: [],
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: cloudflareChoice,
|
||||
description: 'Cloudflare DNS servers',
|
||||
domains: [],
|
||||
primary: true,
|
||||
nameservers: [
|
||||
{
|
||||
ip: "1.1.1.1",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
{
|
||||
ip: "1.0.0.1",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
],
|
||||
groups: [],
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: quad9Choice,
|
||||
description: 'Quad9 DNS servers',
|
||||
domains: [],
|
||||
primary: true,
|
||||
nameservers: [
|
||||
{
|
||||
ip: "9.9.9.9",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
{
|
||||
ip: "149.112.112.112",
|
||||
ns_type: "udp",
|
||||
port: 53,
|
||||
},
|
||||
],
|
||||
groups: [],
|
||||
enabled: true,
|
||||
},
|
||||
]
|
||||
|
||||
const handleSelectChange = (value: string) => {
|
||||
console.log(`selected ${value}`);
|
||||
let nsGroupLocal = {} as NameServerGroup
|
||||
if (value === customChoice) {
|
||||
nsGroupLocal = nsGroup
|
||||
} else {
|
||||
defaultDNSOptions.forEach((nsg) => {
|
||||
if (value === nsg.name) {
|
||||
nsGroupLocal = nsg
|
||||
}
|
||||
})
|
||||
}
|
||||
let newFormGroup = {
|
||||
...nsGroupLocal,
|
||||
groups: getGroupNamesFromIDs(nsGroupLocal.groups),
|
||||
} as formNSGroup
|
||||
setFormNSGroup(newFormGroup)
|
||||
form.setFieldsValue(newFormGroup)
|
||||
setSelectCustom(true)
|
||||
};
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
const nsGroupToSave = createNSGroupToSave(values as NameServerGroup)
|
||||
dispatch(nsGroupActions.saveNameServerGroup.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: nsGroupToSave
|
||||
}))
|
||||
|
||||
})
|
||||
.then(() => onCancel())
|
||||
.catch((errorInfo) => {
|
||||
let msg = "please check the fields and try again"
|
||||
if (errorInfo.errorFields) {
|
||||
msg = errorInfo.errorFields[0].errors[0]
|
||||
}
|
||||
message.error({
|
||||
content: msg,
|
||||
duration: 1,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const createNSGroupToSave = (values: NameServerGroup): NameServerGroupToSave => {
|
||||
let [existingGroups, newGroups] = getExistingAndToCreateGroupsLists(values.groups)
|
||||
return {
|
||||
id: formNSGroup.id || null,
|
||||
name: values.name ? values.name : formNSGroup.name,
|
||||
description: values.description ? values.description : formNSGroup.description,
|
||||
primary: values.primary,
|
||||
domains: values.primary ? [] : values.domains,
|
||||
nameservers: values.nameservers,
|
||||
groups: existingGroups,
|
||||
groupsToCreate: newGroups,
|
||||
enabled: values.enabled,
|
||||
} as NameServerGroupToSave
|
||||
}
|
||||
|
||||
const toggleEditName = (status: boolean) => {
|
||||
setEditName(status)
|
||||
}
|
||||
|
||||
const toggleEditDescription = (status: boolean) => {
|
||||
setEditDescription(status)
|
||||
}
|
||||
|
||||
const domainRegex = /(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/;
|
||||
|
||||
const domainValidator = (_: RuleObject, domain: string) => {
|
||||
if (domainRegex.test(domain)) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
setIsPrimary(false)
|
||||
return Promise.reject(new Error("Please enter a valid domain, e.g. example.com or intra.example.com"))
|
||||
}
|
||||
|
||||
const nameValidator = (_: RuleObject, value: string) => {
|
||||
const found = nsGroupData.find(u => u.name == value && u.id !== formNSGroup.id)
|
||||
if (found) {
|
||||
return Promise.reject(new Error("Please enter a unique name for your nameserver configuration"))
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const ipValidator = (_: RuleObject, value: string) => {
|
||||
if (!cidrRegex().test(value + "/32")) {
|
||||
return Promise.reject(new Error("Please enter a valid IP, e.g. 192.168.1.1 or 8.8.8.8"))
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const formListValidator = (_: RuleObject, names) => {
|
||||
if (names.length >= 3) {
|
||||
return Promise.reject(new Error("Exceeded maximum number of Nameservers. (Max is 2)"));
|
||||
}
|
||||
if (names.length < 1) {
|
||||
return Promise.reject(new Error("You should add at least 1 Nameserver"));
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const primaryValidator = (_: RuleObject, primary: boolean) => {
|
||||
if (!primary && form.getFieldValue("domains").length === 0) {
|
||||
return Promise.reject(new Error("You should select between Resolve all domains or add one Match domain"));
|
||||
}
|
||||
|
||||
if (primary && form.getFieldValue("domains").length > 0) {
|
||||
return Promise.reject(new Error("You should remove all match domains before setting this to yes"));
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const renderNSList = (fields: FormListFieldData[], {add, remove}, {errors}) => (
|
||||
<>
|
||||
<Row>Nameservers</Row>
|
||||
{!!fields.length && (
|
||||
|
||||
<Row align='middle'>
|
||||
<Col span={6} style={{textAlign: 'center'}}>
|
||||
<Typography.Text>Protocol</Typography.Text>
|
||||
</Col>
|
||||
<Col span={10} style={{textAlign: 'center'}}>
|
||||
<Typography.Text>Nameserver IP</Typography.Text>
|
||||
</Col>
|
||||
<Col span={4} style={{textAlign: 'center'}}>
|
||||
<Typography.Text>Port</Typography.Text>
|
||||
</Col>
|
||||
<Col span={2}/>
|
||||
</Row>
|
||||
)}
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<Row key={index}>
|
||||
<Col span={6} style={{textAlign: 'center'}}>
|
||||
<Form.Item style={{margin: '3px'}}
|
||||
name={[field.name, 'ns_type']}
|
||||
rules={[{required: true, message: 'Missing first protocol'}]}
|
||||
initialValue={"udp"}
|
||||
>
|
||||
<Select disabled style={{width: '100%'}}>
|
||||
<Option value="udp">UDP</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={10} style={{margin: '1px'}}>
|
||||
<Form.Item style={{margin: '1px'}}
|
||||
name={[field.name, 'ip']}
|
||||
rules={[{validator: ipValidator}]}
|
||||
>
|
||||
<Input placeholder="e.g. X.X.X.X" style={{width: '100%'}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={4} style={{textAlign: 'center'}}>
|
||||
<Form.Item style={{margin: '1px'}}
|
||||
name={[field.name, 'port']}
|
||||
rules={[{required: true, message: 'Missing port'}]}
|
||||
initialValue={53}
|
||||
>
|
||||
<InputNumber placeholder="Port" style={{width: '100%'}}/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2} style={{textAlign: 'center'}}>
|
||||
<MinusCircleOutlined onClick={() => remove(field.name)}/>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
})}
|
||||
|
||||
<Form.Item>
|
||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined/>}>
|
||||
Add nameserver
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.ErrorList errors={errors}/>
|
||||
</>
|
||||
)
|
||||
|
||||
// @ts-ignore
|
||||
const renderDomains = (fields: FormListFieldData[], {add, remove}, {errors}) => (
|
||||
<>
|
||||
<Row>
|
||||
<Space>
|
||||
<Col>
|
||||
Match domains
|
||||
</Col>
|
||||
<Col>
|
||||
<Tooltip title="Only queries to domains specified here will be resolved by these nameservers."
|
||||
className={"ant-form-item-tooltip"}>
|
||||
<QuestionCircleOutlined style={{color: "rgba(0, 0, 0, 0.45)", cursor: "help"}}/>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
</Space>
|
||||
</Row>
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<Row key={index}>
|
||||
<Col span={20} style={{margin: '1px'}}>
|
||||
<Form.Item hidden={isPrimary} style={{margin: '1px'}}
|
||||
{...field}
|
||||
rules={[{validator: domainValidator}]}
|
||||
>
|
||||
<Input placeholder="e.g. example.com" style={{width: '100%'}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={2} style={{textAlign: 'center'}}>
|
||||
<MinusCircleOutlined hidden={isPrimary} className="dynamic-delete-button"
|
||||
onClick={() => remove(field.name)}/>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
})}
|
||||
|
||||
<Form.Item>
|
||||
<Button type="dashed" disabled={isPrimary} onClick={() => add()} block icon={<PlusOutlined/>}>
|
||||
Add domain
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.ErrorList errors={errors}/>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{nsGroup &&
|
||||
<Drawer
|
||||
headerStyle={{display: "none"}}
|
||||
forceRender={true}
|
||||
open={setupNewNameServerGroupVisible}
|
||||
bodyStyle={{paddingBottom: 80}}
|
||||
onClose={onCancel}
|
||||
autoFocus={true}
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button onClick={onCancel} disabled={savedNSGroup.loading}>Cancel</Button>
|
||||
<Button type="primary" onClick={handleFormSubmit} disabled={savedNSGroup.loading}
|
||||
>{`${formNSGroup.id ? 'Save' : 'Create'}`}</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{selectCustom ?
|
||||
(<Form layout="vertical" requiredMark={false} form={form}
|
||||
onValuesChange={onChange}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Header style={{margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px"}}>
|
||||
<Row align="top">
|
||||
<Col flex="none" style={{display: "flex"}}>
|
||||
{!editName && !editDescription && formNSGroup.id &&
|
||||
<button type="button" aria-label="Close"
|
||||
className="ant-drawer-close"
|
||||
style={{paddingTop: 3}}
|
||||
onClick={onCancel}>
|
||||
<span role="img" aria-label="close"
|
||||
className="anticon anticon-close">
|
||||
<CloseOutlined size={16}/>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
{!editName && formNSGroup.id ? (
|
||||
<div className={"access-control input-text ant-drawer-title"}
|
||||
onClick={() => toggleEditName(true)}>{formNSGroup.id ? formNSGroup.name : 'New nameserver group'}</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Name"
|
||||
tooltip="Add a nameserver group name"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please add an identifier for this nameserver group',
|
||||
whitespace: true
|
||||
},
|
||||
{
|
||||
validator: nameValidator
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input placeholder="e.g. Public DNS" ref={inputNameRef}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)} autoComplete="off"
|
||||
maxLength={40}/>
|
||||
</Form.Item>
|
||||
)}
|
||||
{!editDescription ? (
|
||||
<div className={"access-control input-text ant-drawer-subtitle"}
|
||||
onClick={() => toggleEditDescription(true)}>
|
||||
{formNSGroup.description && formNSGroup.description.trim() !== "" ? formNSGroup.description : 'Add description...'}
|
||||
</div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="Description"
|
||||
style={{marginTop: 24}}
|
||||
>
|
||||
<Input placeholder="Add description..."
|
||||
ref={inputDescriptionRef}
|
||||
onPressEnter={() => toggleEditDescription(false)}
|
||||
onBlur={() => toggleEditDescription(false)}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align="top">
|
||||
<Col flex="auto">
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
</Header>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
|
||||
</Col>
|
||||
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="enabled"
|
||||
label="Status"
|
||||
>
|
||||
<Radio.Group
|
||||
options={optionsDisabledEnabled}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24} flex="auto">
|
||||
<Form.List
|
||||
name="nameservers"
|
||||
rules={[{validator: formListValidator}]}
|
||||
>
|
||||
{renderNSList}
|
||||
</Form.List>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="primary"
|
||||
label="Resolve all domains"
|
||||
rules={[{validator: primaryValidator}]}
|
||||
dependencies={['domains']} // trigger primaryValidation if domains is updated
|
||||
tooltip="Defines if the nameservers are resolvers for all domains"
|
||||
>
|
||||
<Radio.Group
|
||||
options={optionsPrimary}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24} flex="auto">
|
||||
<Form.List
|
||||
name="domains"
|
||||
>
|
||||
{renderDomains}
|
||||
</Form.List>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="groups"
|
||||
label="Distribution groups"
|
||||
tooltip="Distribution groups define to which group of peers these settings will be distributed to"
|
||||
rules={[{validator: selectValidator}]}
|
||||
>
|
||||
<Select mode="tags"
|
||||
style={{width: '100%'}}
|
||||
placeholder="Associate groups with the NS group"
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangeTags}
|
||||
dropdownRender={dropDownRender}
|
||||
>
|
||||
{
|
||||
tagGroups.map(m =>
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
|
||||
<Col span={24}>
|
||||
<Row wrap={false} gutter={12}>
|
||||
<Col flex="none">
|
||||
<FlagFilled/>
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
<Paragraph>
|
||||
Nameservers let you define resolvers for your DNS queries.
|
||||
Because not all operating systems support match-only domain resolution,
|
||||
you should define at least one set of nameservers to resolve all domains
|
||||
per distribution group.
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Divider></Divider>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
|
||||
href="https://netbird.io/docs/how-to-guides/nameservers">Learn more about nameservers</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>) :
|
||||
(
|
||||
<Space direction={"vertical"} style={{width: '100%'}}>
|
||||
<Row align='middle'>
|
||||
<Col span={24} style={{textAlign: 'left'}}>
|
||||
<span className="ant-form-item">Select a predefined nameserver</span>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align='middle'>
|
||||
<Col span={24} style={{textAlign: 'center'}}>
|
||||
<Select
|
||||
style={{width: '100%'}}
|
||||
onChange={handleSelectChange}
|
||||
options={[
|
||||
{
|
||||
value: googleChoice,
|
||||
label: googleChoice,
|
||||
},
|
||||
{
|
||||
value: cloudflareChoice,
|
||||
label: cloudflareChoice,
|
||||
},
|
||||
{
|
||||
value: quad9Choice,
|
||||
label: quad9Choice,
|
||||
},
|
||||
{
|
||||
value: customChoice,
|
||||
label: customChoice,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align='middle'>
|
||||
<Col span={24} style={{textAlign: 'left'}}>
|
||||
<Col span={24} style={{textAlign: 'left'}}>
|
||||
<span className="ant-form-item"><Typography.Link
|
||||
onClick={() => handleSelectChange(customChoice)}>Or add custom</Typography.Link></span>
|
||||
</Col>
|
||||
</Col>
|
||||
</Row>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
|
||||
</Drawer>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NameServerGroupUpdate
|
||||
@@ -1,49 +1,77 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {Link, useLocation} from 'react-router-dom';
|
||||
import logo from "../assets/logo.png";
|
||||
import {useLocation} from 'react-router-dom';
|
||||
import {Menu, Row, Col, Grid, Dropdown, Avatar, Button, Typography, Space} from 'antd'
|
||||
import {Avatar, Button, Col, Dropdown, Grid, Menu, Row} from 'antd'
|
||||
import {ItemType} from "antd/lib/menu/hooks/useItems";
|
||||
import {AvatarSize} from "antd/es/avatar/SizeContext";
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import { useOidc,useOidcUser } from '@axa-fr/react-oidc';
|
||||
import {UserOutlined} from '@ant-design/icons';
|
||||
import {useOidc, useOidcIdToken, useOidcUser} from '@axa-fr/react-oidc';
|
||||
import {getConfig} from "../config";
|
||||
const { Text } = Typography
|
||||
const { useBreakpoint } = Grid;
|
||||
import {User} from "../store/user/types";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as userActions} from "../store/user";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {actions as personalAccessTokenActions} from "../store/personal-access-token";
|
||||
|
||||
const {useBreakpoint} = Grid;
|
||||
|
||||
const Navbar = () => {
|
||||
let location = useLocation();
|
||||
const config = getConfig();
|
||||
const {
|
||||
isAuthenticated,
|
||||
logout,
|
||||
} = useOidc();
|
||||
const { logout } = useOidc();
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const { oidcUser } = useOidcUser();
|
||||
const {oidcUser} = useOidcUser();
|
||||
const {idTokenPayload} = useOidcIdToken()
|
||||
const user = oidcUser;
|
||||
const [currentUser, setCurrentUser] = useState({} as User)
|
||||
|
||||
const screens = useBreakpoint();
|
||||
|
||||
const [hideMenuUser, setHideMenuUser] = useState(false)
|
||||
const users = useSelector((state: RootState) => state.user.data)
|
||||
const [isRefreshingUserState, setIsRefreshingUserState] = useState(false)
|
||||
|
||||
const items = [
|
||||
{label: (<Link to="/peers">Peers</Link>), key: '/peers'},
|
||||
{label: (<Link to="/setup-keys">Setup Keys</Link>), key: '/setup-keys'},
|
||||
{label: (<Link to="/acls">Access Control</Link>), key: '/acls'},
|
||||
{label: (<Link to="/routes">Network Routes</Link>), key: '/routes'},
|
||||
{ label: (<Link to="/dns">DNS</Link>), key: '/dns' },
|
||||
{label: (<Link to="/users">Users</Link>), key: '/users'},
|
||||
{label: (<Link to="/activity">Activity</Link>), key: '/activity'},
|
||||
{label: (<Link to="/settings">Settings</Link>), key: '/settings'}
|
||||
] as ItemType[]
|
||||
|
||||
const userEmailKey = 'user-email'
|
||||
const userLogoutKey = 'user-logout'
|
||||
const userDividerKey = 'user-divider'
|
||||
const [menuItems, setMenuItems] = useState([
|
||||
{ label: (<Link to="/peers">Peers</Link>), key: '/peers' },
|
||||
{ label: (<Link to="/add-peer">Add Peer</Link>), key: '/add-peer' },
|
||||
{ label: (<Link to="/setup-keys">Setup Keys</Link>), key: '/setup-keys' },
|
||||
{ label: (<Link to="/acls">Access Control</Link>), key: '/acls' },
|
||||
{ label: (<Link to="/routes">Network Routes</Link>), key: '/routes' },
|
||||
{ label: (<Link to="/users">Users</Link>), key: '/users' }
|
||||
] as ItemType[])
|
||||
const adminOnlyTabs = ["/setup-keys", "/acls", "/routes", "/dns", "/activity", "/settings"]
|
||||
const [menuItems, setMenuItems] = useState(items)
|
||||
const logoutWithRedirect = () =>
|
||||
logout("/",{client_id:config.clientId});
|
||||
logout("/", {client_id: config.clientId});
|
||||
|
||||
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 = menuItems.filter(m => m?.key !== userEmailKey && m?.key !== userLogoutKey && m?.key !== userDividerKey)
|
||||
const fs = items.filter(m => showTab(m?.key?.toString(), currentUser) && m?.key !== userEmailKey && m?.key !== userLogoutKey && m?.key !== userDividerKey)
|
||||
if (screens.xs === true) {
|
||||
setHideMenuUser(false)
|
||||
fs.push({ type: 'divider', key: userDividerKey })
|
||||
fs.push({type: 'divider', key: userDividerKey})
|
||||
fs.push({
|
||||
label: (
|
||||
<Link to="#">{user?.name}</Link>
|
||||
@@ -51,32 +79,70 @@ const Navbar = () => {
|
||||
icon: createAvatar("small"),
|
||||
key: userEmailKey
|
||||
})
|
||||
fs.push({ label: (<Button type="link" block onClick={logoutWithRedirect}>Logout</Button>), key: userLogoutKey })
|
||||
fs.push({
|
||||
label: (<Button type="link" block onClick={logoutWithRedirect}>Logout</Button>),
|
||||
key: userLogoutKey
|
||||
})
|
||||
setMenuItems([...fs])
|
||||
return
|
||||
}
|
||||
setMenuItems([...fs])
|
||||
setHideMenuUser(true)
|
||||
}, [screens])
|
||||
}, [screens, currentUser])
|
||||
|
||||
useEffect(() => {
|
||||
if (users.length === 0 && !isRefreshingUserState &&
|
||||
window.location.pathname !== '/peers' &&
|
||||
window.location.pathname !== '/users') {
|
||||
setIsRefreshingUserState(true)
|
||||
dispatch(userActions.getUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}))
|
||||
return
|
||||
}
|
||||
if (users.length === 0 && isRefreshingUserState) {
|
||||
return
|
||||
}
|
||||
let runUser = oidcUser
|
||||
if (!oidcUser) {
|
||||
runUser = idTokenPayload
|
||||
}
|
||||
setIsRefreshingUserState(false)
|
||||
if (runUser) {
|
||||
const found = users.find(u => u.is_current ? u.is_current : runUser.sub ? u.id == runUser.sub : false)
|
||||
if (found) {
|
||||
setCurrentUser(found)
|
||||
}
|
||||
}
|
||||
}, [users, oidcUser])
|
||||
|
||||
const showTab = (key: string | undefined, user: User | undefined) => {
|
||||
if (!user) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (user.role?.toLowerCase() === "admin") {
|
||||
return true
|
||||
}
|
||||
return !adminOnlyTabs.find(t => t === key)
|
||||
}
|
||||
|
||||
const menuUser = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
label: <>{user?.email}</>,
|
||||
label: (<Link to="/users" onClick={openPersonalUserPage}>{user?.email}</Link>),
|
||||
key: '0',
|
||||
},
|
||||
{
|
||||
label: (<Link to="/logout" onClick={logoutWithRedirect}>Logout</Link>),
|
||||
label: (<Link to="/logout" onClick={logoutWithRedirect}>Logout</Link>),
|
||||
key: '1',
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const createAvatar = (size:AvatarSize) => {
|
||||
const createAvatar = (size: AvatarSize) => {
|
||||
return user?.picture ? (
|
||||
<Avatar size={size} src={user?.picture} icon={<UserOutlined />} />
|
||||
<Avatar size={size} src={user?.picture} icon={<UserOutlined/>}/>
|
||||
) : (
|
||||
<Avatar size={size}>{(user?.name || '').slice(0, 1).toUpperCase()}</Avatar>
|
||||
)
|
||||
@@ -96,7 +162,12 @@ const Navbar = () => {
|
||||
</Col>
|
||||
<Col flex="1 1 auto">
|
||||
<div>
|
||||
<Menu mode="horizontal" selectable={true} selectedKeys={[location.pathname]} defaultSelectedKeys={[location.pathname]} items={menuItems}/>
|
||||
<Menu mode="horizontal" selectable={true} selectedKeys={[location.pathname]}
|
||||
onSelect={(e) => {
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null))
|
||||
}}
|
||||
defaultSelectedKeys={[location.pathname]} items={menuItems}/>
|
||||
</div>
|
||||
</Col>
|
||||
{hideMenuUser &&
|
||||
|
||||
@@ -2,23 +2,29 @@ import React, {useEffect, useRef, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as peerActions} from '../store/peer';
|
||||
import {Button, Col, Divider, Drawer, Form, Input, Row, Select, Space, Tag, Typography} from "antd";
|
||||
import {Button, Col, Collapse, Divider, Drawer, Form, Input, Radio, Row, Select, Space, Tag, Typography} from "antd";
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import type {CustomTagProps} from 'rc-select/lib/BaseSelect'
|
||||
import {Peer, PeerGroupsToSave} from "../store/peer/types";
|
||||
import {FormPeer, Peer, PeerGroupsToSave} from "../store/peer/types";
|
||||
import {Group, GroupPeer} from "../store/group/types";
|
||||
import {CloseOutlined, EditOutlined, FlagFilled} from "@ant-design/icons";
|
||||
import {CloseOutlined, EditOutlined} from "@ant-design/icons";
|
||||
import {RuleObject} from 'antd/lib/form';
|
||||
import { useOidcAccessToken } from '@axa-fr/react-oidc';
|
||||
const { Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {timeAgo} from "../utils/common";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
const {Option} = Select;
|
||||
const {Panel} = Collapse;
|
||||
const punycode = require('punycode/')
|
||||
|
||||
const PeerUpdate = () => {
|
||||
const { accessToken } = useOidcAccessToken()
|
||||
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const peer = useSelector((state: RootState) => state.peer.peer)
|
||||
const [formPeer, setFormPeer] = useState({} as Peer)
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const users = useSelector((state: RootState) => state.user.data)
|
||||
const peer: Peer = useSelector((state: RootState) => state.peer.peer)
|
||||
const [formPeer, setFormPeer] = useState({} as FormPeer)
|
||||
const updateGroupsVisible = useSelector((state: RootState) => state.peer.updateGroupsVisible)
|
||||
const savedGroups = useSelector((state: RootState) => state.peer.savedGroups)
|
||||
const updatedPeers = useSelector((state: RootState) => state.peer.updatedPeer)
|
||||
@@ -28,6 +34,7 @@ const PeerUpdate = () => {
|
||||
const [peerGroups, setPeerGroups] = useState([] as GroupPeer[])
|
||||
const inputNameRef = useRef<any>(null)
|
||||
const [editName, setEditName] = useState(false)
|
||||
const [estimatedName, setEstimatedName] = useState("")
|
||||
const [callingPeerAPI, setCallingPeerAPI] = useState(false)
|
||||
const [callingGroupAPI, setCallingGroupAPI] = useState(false)
|
||||
const [isSubmitRunning, setSubmitRunning] = useState(false)
|
||||
@@ -46,21 +53,21 @@ const PeerUpdate = () => {
|
||||
if (callingPeerAPI && updatedPeers.success) {
|
||||
setCallingPeerAPI(false)
|
||||
}
|
||||
},[updatedPeers])
|
||||
}, [updatedPeers])
|
||||
|
||||
// wait save groups to succeed
|
||||
useEffect(() => {
|
||||
if (callingGroupAPI && savedGroups.success) {
|
||||
setCallingGroupAPI(false)
|
||||
}
|
||||
},[savedGroups])
|
||||
}, [savedGroups])
|
||||
|
||||
// clean temp state and close
|
||||
useEffect(() => {
|
||||
if (isSubmitRunning && !callingGroupAPI && !callingPeerAPI) {
|
||||
onCancel()
|
||||
}
|
||||
},[callingGroupAPI,callingPeerAPI])
|
||||
}, [callingGroupAPI, callingPeerAPI])
|
||||
|
||||
useEffect(() => {
|
||||
if (editName) inputNameRef.current!.focus({
|
||||
@@ -74,18 +81,26 @@ const PeerUpdate = () => {
|
||||
const gs_name = gs?.map(g => g.name) as string[]
|
||||
setPeerGroups(gs)
|
||||
setSelectedTagGroups(gs_name)
|
||||
setFormPeer(peer)
|
||||
form.setFieldsValue({
|
||||
name: formPeer.name ? formPeer.name: peer.name,
|
||||
groups: gs_name
|
||||
})
|
||||
const fPeer = {
|
||||
...peer,
|
||||
name: formPeer.name ? formPeer.name : peer.name,
|
||||
groupsNames: gs_name,
|
||||
userEmail: users?.find(u => u.id === peer.user_id)?.email,
|
||||
last_seen: peer.connected ? "just now" : String(timeAgo(peer.last_seen)),
|
||||
ui_version: peer.ui_version ? peer.ui_version.replace("netbird-desktop-ui/", "") : ""
|
||||
} as FormPeer
|
||||
setFormPeer(fPeer)
|
||||
form.setFieldsValue(fPeer)
|
||||
}, [peer])
|
||||
|
||||
useEffect(() => {
|
||||
setTagGroups(groups?.map(g => g.name) || [])
|
||||
}, [groups])
|
||||
|
||||
const toggleEditName = (status:boolean) => {
|
||||
useEffect(() => {
|
||||
}, [users])
|
||||
|
||||
const toggleEditName = (status: boolean) => {
|
||||
setEditName(status)
|
||||
}
|
||||
|
||||
@@ -103,7 +118,7 @@ const PeerUpdate = () => {
|
||||
}, [selectedTagGroups])
|
||||
|
||||
const tagRender = (props: CustomTagProps) => {
|
||||
const { label, value, closable, onClose } = props;
|
||||
const {label, value, closable, onClose} = props;
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -120,7 +135,7 @@ const PeerUpdate = () => {
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={tagClosable}
|
||||
onClose={onClose}
|
||||
style={{ marginRight: 3 }}
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{value}</strong>
|
||||
</Tag>
|
||||
@@ -130,12 +145,12 @@ const PeerUpdate = () => {
|
||||
const optionRender = (label: string) => {
|
||||
let peersCount = ''
|
||||
const g = groups.find(_g => _g.name === label)
|
||||
if (g) peersCount = ` - ${g.peers_count || 0} ${(!g.peers_count || parseInt(g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
if (g) peersCount = ` - ${g.peers_count || 0} ${(!g.peers_count || parseInt(g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{ marginRight: 3 }}
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{label}</strong>
|
||||
</Tag>
|
||||
@@ -147,21 +162,23 @@ const PeerUpdate = () => {
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{ margin: '8px 0' }} />
|
||||
<Divider style={{margin: '8px 0'}}/>
|
||||
<Row style={{padding: '0 8px 4px'}}>
|
||||
<Col flex="auto">
|
||||
<span style={{color: "#9CA3AF"}}>Add new group by pressing "Enter"</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z" fill="#9CA3AF"/>
|
||||
<path
|
||||
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
|
||||
fill="#9CA3AF"/>
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
|
||||
const setUpdateGroupsVisible = (status:boolean) => {
|
||||
const setUpdateGroupsVisible = (status: boolean) => {
|
||||
dispatch(peerActions.setUpdateGroupsVisible(status));
|
||||
}
|
||||
|
||||
@@ -170,27 +187,32 @@ const PeerUpdate = () => {
|
||||
setUpdateGroupsVisible(false)
|
||||
setEditName(false)
|
||||
// setSaveBtnDisabled(true)
|
||||
setFormPeer({} as Peer)
|
||||
setFormPeer({} as FormPeer)
|
||||
setCallingPeerAPI(false)
|
||||
setCallingPeerAPI(false)
|
||||
setSubmitRunning(false)
|
||||
setEstimatedName("")
|
||||
}
|
||||
|
||||
const noUpdateToGroups = ():Boolean => {
|
||||
const noUpdateToGroups = (): Boolean => {
|
||||
return !peerGroupsToSave.groupsToRemove.length && !peerGroupsToSave.groupsToAdd.length && !peerGroupsToSave.groupsNoId.length
|
||||
}
|
||||
|
||||
const noUpdateToName = ():Boolean => {
|
||||
const noUpdateToName = (): Boolean => {
|
||||
return !formPeer.name || formPeer.name === peer.name
|
||||
}
|
||||
|
||||
const onChange = (data:any) => {
|
||||
const noUpdateToLoginExpiration = (): Boolean => {
|
||||
return formPeer.login_expiration_enabled === peer.login_expiration_enabled
|
||||
}
|
||||
|
||||
const onChange = (data: any) => {
|
||||
setFormPeer({...formPeer, ...data})
|
||||
}
|
||||
|
||||
const handleChangeTags = (value: string[]) => {
|
||||
let validatedValues: string[] = []
|
||||
value.forEach(function(v) {
|
||||
value.forEach(function (v) {
|
||||
if (v.trim().length) {
|
||||
validatedValues.push(v)
|
||||
}
|
||||
@@ -198,11 +220,26 @@ const PeerUpdate = () => {
|
||||
setSelectedTagGroups(validatedValues)
|
||||
};
|
||||
|
||||
const createPeerToSave = ():Peer => {
|
||||
const nameValidator = (_: RuleObject, value: string) => {
|
||||
let punyName = punycode.toASCII(value.toLowerCase())
|
||||
let domain = ""
|
||||
if (formPeer.dns_label) {
|
||||
let labelList = formPeer.dns_label.split(".")
|
||||
if (labelList.length > 1) {
|
||||
labelList.splice(0,1)
|
||||
domain = "." + labelList.join(".")
|
||||
}
|
||||
}
|
||||
setEstimatedName(punyName+domain)
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const createPeerToSave = (): Peer => {
|
||||
return {
|
||||
id: formPeer.id,
|
||||
ssh_enabled: formPeer.ssh_enabled,
|
||||
name: formPeer.name
|
||||
name: formPeer.name,
|
||||
login_expiration_enabled: formPeer.login_expiration_enabled
|
||||
} as Peer
|
||||
}
|
||||
|
||||
@@ -210,14 +247,20 @@ const PeerUpdate = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
setSubmitRunning(true)
|
||||
if (!noUpdateToName()) {
|
||||
if (!noUpdateToName() || !noUpdateToLoginExpiration()) {
|
||||
const peerUpdate = createPeerToSave()
|
||||
setCallingPeerAPI(true)
|
||||
dispatch(peerActions.updatePeer.request({getAccessTokenSilently:accessToken, payload: peerUpdate}))
|
||||
dispatch(peerActions.updatePeer.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: peerUpdate
|
||||
}))
|
||||
}
|
||||
if (peerGroupsToSave.groupsToRemove.length || peerGroupsToSave.groupsToAdd.length || peerGroupsToSave.groupsNoId.length) {
|
||||
if (!noUpdateToGroups()) {
|
||||
setCallingGroupAPI(true)
|
||||
dispatch(peerActions.saveGroups.request({getAccessTokenSilently:accessToken, payload: peerGroupsToSave}))
|
||||
dispatch(peerActions.saveGroups.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: peerGroupsToSave
|
||||
}))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
@@ -233,7 +276,7 @@ const PeerUpdate = () => {
|
||||
return Promise.reject(new Error("Please enter ate least one group"))
|
||||
}
|
||||
|
||||
value.forEach(function(v: string) {
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v)
|
||||
}
|
||||
@@ -259,25 +302,28 @@ const PeerUpdate = () => {
|
||||
<Drawer
|
||||
forceRender={true}
|
||||
headerStyle={{display: "none"}}
|
||||
visible={true}
|
||||
open={true}
|
||||
bodyStyle={{paddingBottom: 80}}
|
||||
onClose={onCancel}
|
||||
autoFocus={true}
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button onClick={onCancel} disabled={savedGroups.loading}>Cancel</Button>
|
||||
<Button type="primary" disabled={(savedGroups.loading || updatedPeers.loading) || (noUpdateToGroups() && noUpdateToName())} onClick={handleFormSubmit}>Save</Button>
|
||||
<Button type="primary"
|
||||
disabled={(savedGroups.loading || updatedPeers.loading) || (noUpdateToGroups() && noUpdateToName() && noUpdateToLoginExpiration())}
|
||||
onClick={handleFormSubmit}>Save</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form layout="vertical" hideRequiredMark form={form} onValuesChange={onChange}>
|
||||
<Form layout="vertical" requiredMark={false} form={form} onValuesChange={onChange}>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Header style={{margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px"}}>
|
||||
<Row align="top">
|
||||
<Col flex="none" style={{display: "flex"}}>
|
||||
{!editName && peer.id &&
|
||||
<button type="button" aria-label="Close" className="ant-drawer-close"
|
||||
<button type="button" aria-label="Close" autoFocus={true}
|
||||
className="ant-drawer-close"
|
||||
style={{paddingTop: 3}}
|
||||
onClick={onCancel}>
|
||||
<span role="img" aria-label="close"
|
||||
@@ -293,46 +339,135 @@ const PeerUpdate = () => {
|
||||
onClick={() => toggleEditName(true)}>{formPeer.name ? formPeer.name : peer.name}
|
||||
<EditOutlined/></div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Name"
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please add a new name for this peer',
|
||||
whitespace: true
|
||||
}]}
|
||||
style={{display: 'flex'}}
|
||||
>
|
||||
<Input
|
||||
placeholder={peer.name}
|
||||
ref={inputNameRef}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)}
|
||||
// onChange={(e) => handleChangeName(e.)}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>)}
|
||||
<Row>
|
||||
<Space direction={"vertical"} size="small">
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Name"
|
||||
style={{margin: '1px'}}
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please add a new name for this peer',
|
||||
whitespace: true
|
||||
},{validator:nameValidator}]}
|
||||
>
|
||||
<Input
|
||||
placeholder={peer.name}
|
||||
ref={inputNameRef}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)}
|
||||
autoComplete="off"
|
||||
max={59}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="New peer domain name preview"
|
||||
tooltip="If the domain name already exists, we add an increment number suffix to it"
|
||||
style={{margin: '1px'}}
|
||||
>
|
||||
<Paragraph>
|
||||
<Tag>
|
||||
{estimatedName}
|
||||
</Tag>
|
||||
</Paragraph>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
|
||||
</Row>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
</Header>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="ip"
|
||||
label={<>
|
||||
<span style={{
|
||||
marginRight: "5px",
|
||||
}}>NetBird IP</span>
|
||||
<Tag
|
||||
color={formPeer.connected ? "green" : "red"}>{formPeer.connected ? "online" : "offline"}</Tag>
|
||||
</>}
|
||||
>
|
||||
<Input
|
||||
disabled={true}
|
||||
value={formPeer.ip}
|
||||
style={{color: "#5a5c5a"}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="last_seen"
|
||||
label="Last seen"
|
||||
>
|
||||
<Input
|
||||
disabled={true}
|
||||
value={formPeer.last_seen}
|
||||
style={{color: "#5a5c5a"}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="groups"
|
||||
label="Select groups to associate with this peer"
|
||||
rules={[{ validator: selectValidator }]}
|
||||
style={{display: 'flex'}}
|
||||
name="dns_label"
|
||||
label="Domain name"
|
||||
>
|
||||
<Input
|
||||
disabled={true}
|
||||
value={formPeer.userEmail}
|
||||
style={{color: "#5a5c5a"}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
{formPeer.user_id && (
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="userEmail"
|
||||
label="User"
|
||||
>
|
||||
<Input
|
||||
disabled={true}
|
||||
value={formPeer.userEmail}
|
||||
style={{color: "#5a5c5a"}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)}
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="login_expiration_enabled"
|
||||
label="Login expiration"
|
||||
tooltip="When login expires, the user has to re-authenticate this peer. This only applies to peers added with the SSO login."
|
||||
>
|
||||
<Radio.Group
|
||||
options={[{label: 'Enabled', value: true}, {label: 'Disabled', value: false}]}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
disabled={!formPeer.user_id}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="groupsNames"
|
||||
label="Select peer groups"
|
||||
rules={[{validator: selectValidator}]}
|
||||
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
style={{width: '100%'}}
|
||||
placeholder="Select groups..."
|
||||
tagRender={tagRender}
|
||||
// onDropdownVisibleChange={evaluateSubmit}
|
||||
// onSelect={evaluateSubmit}
|
||||
|
||||
dropdownRender={dropDownRender}
|
||||
onChange={handleChangeTags}>
|
||||
@@ -345,16 +480,62 @@ const PeerUpdate = () => {
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Row wrap={false} gutter={12}>
|
||||
<Col flex="none">
|
||||
<FlagFilled/>
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
<Paragraph>
|
||||
Every peer is part of the group All, thus you can't remove it.
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
<Collapse onChange={onChange} bordered={false} ghost={true}
|
||||
style={{color: "#5a5c5a"}}>
|
||||
<Panel key="0" header="System Info">
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="hostname"
|
||||
label="Hostname"
|
||||
>
|
||||
<Input
|
||||
disabled={true}
|
||||
value={formPeer.hostname}
|
||||
style={{color: "#5a5c5a"}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="os"
|
||||
label="Operating system"
|
||||
>
|
||||
<Input
|
||||
disabled={true}
|
||||
value={formPeer.os}
|
||||
style={{color: "#5a5c5a"}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="version"
|
||||
label="Agent version"
|
||||
>
|
||||
<Input
|
||||
disabled={true}
|
||||
value={formPeer.os}
|
||||
style={{color: "#5a5c5a"}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{formPeer.ui_version && (
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="ui_version"
|
||||
label="UI version"
|
||||
>
|
||||
<Input
|
||||
disabled={true}
|
||||
value={formPeer.ui_version}
|
||||
style={{color: "#5a5c5a"}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>)}
|
||||
</Row>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
@@ -1,46 +1,63 @@
|
||||
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 {actions as routeActions} from '../store/route';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Row,
|
||||
Divider,
|
||||
Drawer,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Radio,
|
||||
Row,
|
||||
Select,
|
||||
SelectProps,
|
||||
Space,
|
||||
Switch,
|
||||
SelectProps,
|
||||
Button, Drawer, Form, Divider, Select, Radio, Typography
|
||||
Typography
|
||||
} from "antd";
|
||||
import {CloseOutlined, FlagFilled, QuestionCircleFilled} from "@ant-design/icons";
|
||||
import {Route} from "../store/route/types";
|
||||
import {Route, RouteToSave} from "../store/route/types";
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import {useOidcAccessToken} from "@axa-fr/react-oidc";
|
||||
import cidrRegex from 'cidr-regex';
|
||||
import {
|
||||
initPeerMaps,
|
||||
masqueradeDisabledMSG,
|
||||
peerToPeerIP,
|
||||
initPeerMaps,
|
||||
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 {
|
||||
}
|
||||
|
||||
const RouteUpdate = () => {
|
||||
const {accessToken} = useOidcAccessToken()
|
||||
const {
|
||||
tagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidator
|
||||
} = 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 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)
|
||||
@@ -56,13 +73,14 @@ const RouteUpdate = () => {
|
||||
const [masqueradeMSG, setMasqueradeMSG] = useState(defaultMasqueradeMSG)
|
||||
const defaultStatusMSG = "Status"
|
||||
const [statusMSG, setStatusMSG] = useState(defaultStatusMSG)
|
||||
const [peerNameToIP, peerIPToName] = initPeerMaps(peers);
|
||||
const [peerNameToIP, peerIPToName, peerIPToID] = initPeerMaps(peers);
|
||||
const [newRoute, setNewRoute] = useState(false)
|
||||
|
||||
const optionsDisabledEnabled = [{label: 'Enabled', value: true}, {label: 'Disabled', value: false}]
|
||||
|
||||
useEffect(() => {
|
||||
if (setupNewRouteHA) {
|
||||
setRoutingPeerMSG("Add additional routing peer")
|
||||
if (!newRoute ) {
|
||||
setRoutingPeerMSG(defaultRoutingPeerMSG)
|
||||
setMasqueradeMSG("Update Masquerade")
|
||||
setStatusMSG("Update Status")
|
||||
} else {
|
||||
@@ -71,7 +89,7 @@ const RouteUpdate = () => {
|
||||
setStatusMSG(defaultStatusMSG)
|
||||
setPreviousRouteKey("")
|
||||
}
|
||||
}, [setupNewRouteHA])
|
||||
}, [newRoute])
|
||||
|
||||
useEffect(() => {
|
||||
if (editName) inputNameRef.current!.focus({
|
||||
@@ -90,33 +108,46 @@ const RouteUpdate = () => {
|
||||
|
||||
const fRoute = {
|
||||
...route,
|
||||
groups: getGroupNamesFromIDs(route.groups)
|
||||
} as FormRoute
|
||||
setFormRoute(fRoute)
|
||||
setPreviousRouteKey(fRoute.network_id+fRoute.network)
|
||||
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
|
||||
let os: string
|
||||
os = p.os
|
||||
if (!os.toLowerCase().startsWith("darwin") && !os.toLowerCase().startsWith("windows")) {
|
||||
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),
|
||||
label: peerToPeerIP(p.name, p.ip),
|
||||
value: peerToPeerIP(p.name, p.ip),
|
||||
disabled: false
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const createRouteToSave = (inputRoute:FormRoute):Route => {
|
||||
const createRouteToSave = (inputRoute: FormRoute): RouteToSave => {
|
||||
let peerIDList = inputRoute.peer.split(routePeerSeparator)
|
||||
let peerID:string
|
||||
if (peerIDList[1]) {
|
||||
peerID = peerIDList[1]
|
||||
let peerID: string
|
||||
if (peerIDList.length === 1) {
|
||||
peerID = inputRoute.peer
|
||||
} else {
|
||||
peerID = peerNameToIP[inputRoute.peer]
|
||||
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,
|
||||
@@ -125,8 +156,10 @@ const RouteUpdate = () => {
|
||||
peer: peerID,
|
||||
enabled: inputRoute.enabled,
|
||||
masquerade: inputRoute.masquerade,
|
||||
metric: inputRoute.metric
|
||||
} as Route
|
||||
metric: inputRoute.metric,
|
||||
groups: existingGroups,
|
||||
groupsToCreate: groupsToCreate,
|
||||
} as RouteToSave
|
||||
}
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
@@ -134,13 +167,16 @@ const RouteUpdate = () => {
|
||||
.then(() => {
|
||||
if (!setupNewRouteHA || formRoute.peer != '') {
|
||||
const routeToSave = createRouteToSave(formRoute)
|
||||
dispatch(routeActions.saveRoute.request({getAccessTokenSilently:accessToken, payload: routeToSave}))
|
||||
dispatch(routeActions.saveRoute.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: routeToSave
|
||||
}))
|
||||
} else {
|
||||
let groupedDataTable = transformGroupedDataTable(routes,peerIPToName)
|
||||
let groupedDataTable = transformGroupedDataTable(routes, peers)
|
||||
groupedDataTable.forEach((group) => {
|
||||
if (group.key == previousRouteKey) {
|
||||
group.groupedRoutes.forEach((route) => {
|
||||
let updateRoute:FormRoute = {
|
||||
let updateRoute: FormRoute = {
|
||||
...formRoute,
|
||||
id: route.id,
|
||||
peer: route.peer,
|
||||
@@ -148,7 +184,10 @@ const RouteUpdate = () => {
|
||||
enabled: (formRoute.enabled != group.enabled) ? formRoute.enabled : route.enabled
|
||||
}
|
||||
const routeToSave = createRouteToSave(updateRoute)
|
||||
dispatch(routeActions.saveRoute.request({getAccessTokenSilently:accessToken, payload: routeToSave}))
|
||||
dispatch(routeActions.saveRoute.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: routeToSave
|
||||
}))
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -160,11 +199,11 @@ const RouteUpdate = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const setVisibleNewRoute = (status:boolean) => {
|
||||
const setVisibleNewRoute = (status: boolean) => {
|
||||
dispatch(routeActions.setSetupNewRouteVisible(status));
|
||||
}
|
||||
|
||||
const setSetupNewRouteHA = (status:boolean) => {
|
||||
const setSetupNewRouteHA = (status: boolean) => {
|
||||
dispatch(routeActions.setSetupNewRouteHA(status));
|
||||
}
|
||||
|
||||
@@ -183,23 +222,24 @@ const RouteUpdate = () => {
|
||||
setVisibleNewRoute(false)
|
||||
setSetupNewRouteHA(false)
|
||||
setPreviousRouteKey("")
|
||||
setNewRoute(false)
|
||||
}
|
||||
|
||||
const onChange = (data:any) => {
|
||||
const onChange = (data: any) => {
|
||||
setFormRoute({...formRoute, ...data})
|
||||
}
|
||||
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
const peerDropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
</>
|
||||
)
|
||||
|
||||
const toggleEditName = (status:boolean) => {
|
||||
const toggleEditName = (status: boolean) => {
|
||||
setEditName(status);
|
||||
}
|
||||
|
||||
const toggleEditDescription = (status:boolean) => {
|
||||
const toggleEditDescription = (status: boolean) => {
|
||||
setEditDescription(status);
|
||||
}
|
||||
|
||||
@@ -215,61 +255,96 @@ const RouteUpdate = () => {
|
||||
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}
|
||||
visible={setupNewRouteVisible}
|
||||
open={setupNewRouteVisible}
|
||||
bodyStyle={{paddingBottom: 80}}
|
||||
onClose={onCancel}
|
||||
autoFocus={true}
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button onClick={onCancel} disabled={savedRoute.loading}>Cancel</Button>
|
||||
<Button type="primary" disabled={savedRoute.loading} onClick={handleFormSubmit}>{`${formRoute.network_id ? 'Save' : 'Create'}`}</Button>
|
||||
<Button type="primary" disabled={savedRoute.loading}
|
||||
onClick={handleFormSubmit}>{`${newRoute ? 'Create' : 'Save'}`}</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form layout="vertical" hideRequiredMark form={form} onValuesChange={onChange}>
|
||||
<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 &&
|
||||
{!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">
|
||||
<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>
|
||||
{!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}]}
|
||||
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}/>
|
||||
<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>
|
||||
{!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}/>
|
||||
<Input placeholder="Add description..." ref={inputDescriptionRef}
|
||||
disabled={!setupNewRouteHA && !newRoute}
|
||||
onPressEnter={() => toggleEditDescription(false)}
|
||||
onBlur={() => toggleEditDescription(false)}
|
||||
autoComplete="off" maxLength={200}/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
@@ -293,7 +368,8 @@ const RouteUpdate = () => {
|
||||
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}/>
|
||||
<Input placeholder="e.g. 172.16.0.0/16" disabled={!setupNewRouteHA && !newRoute}
|
||||
autoComplete="off" minLength={9} maxLength={43}/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
@@ -314,14 +390,15 @@ const RouteUpdate = () => {
|
||||
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={dropDownRender}
|
||||
options={options}
|
||||
allowClear={true}
|
||||
showSearch
|
||||
style={{width: '100%'}}
|
||||
placeholder="Select Peer"
|
||||
dropdownRender={peerDropDownRender}
|
||||
options={options}
|
||||
allowClear={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
@@ -331,7 +408,7 @@ const RouteUpdate = () => {
|
||||
label={masqueradeMSG}
|
||||
tooltip={masqueradeDisabledMSG}
|
||||
>
|
||||
<Switch size={"small"} disabled={!setupNewRouteHA} checked={formRoute.masquerade}/>
|
||||
<Switch size={"small"} disabled={!setupNewRouteHA && !newRoute} checked={formRoute.masquerade}/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
@@ -343,6 +420,28 @@ const RouteUpdate = () => {
|
||||
<InputNumber min={1} max={9999} autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="groups"
|
||||
label="Distribution groups"
|
||||
tooltip="NetBird will advertise this route to peers that belong to the following groups"
|
||||
rules={[{validator: selectPreValidator}]}
|
||||
>
|
||||
<Select mode="tags"
|
||||
style={{width: '100%'}}
|
||||
placeholder="Associate groups with the network route"
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangeTags}
|
||||
dropdownRender={dropDownRender}
|
||||
>
|
||||
{
|
||||
tagGroups.map(m =>
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Row wrap={false} gutter={12}>
|
||||
<Col flex="none">
|
||||
@@ -350,7 +449,8 @@ const RouteUpdate = () => {
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
<Paragraph>
|
||||
You can enable high-availability by assigning the same network identifier and network CIDR to multiple routes.
|
||||
You can enable high-availability by assigning the same network identifier
|
||||
and network CIDR to multiple routes.
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -358,8 +458,7 @@ const RouteUpdate = () => {
|
||||
<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>
|
||||
href="https://netbird.io/docs/how-to-guides/network-routes">Learn more about network routes</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
@@ -1,176 +1,177 @@
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {actions as setupKeyActions} from '../store/setup-key';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
DatePicker,
|
||||
DatePickerProps,
|
||||
Divider,
|
||||
Drawer,
|
||||
Form,
|
||||
Input,
|
||||
List,
|
||||
Radio,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Typography
|
||||
} from "antd";
|
||||
import {actions as setupKeyActions} from "../store/setup-key";
|
||||
import {Button, Col, Divider, Form, Input, InputNumber, Modal, Row, Select, Space, Switch, Tag, Typography} from "antd";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {CloseOutlined, EditOutlined, QuestionCircleFilled} from "@ant-design/icons";
|
||||
import {SetupKey, SetupKeyToSave} from "../store/setup-key/types";
|
||||
import {useOidcAccessToken} from "@axa-fr/react-oidc";
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
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";
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
const {Text} = Typography;
|
||||
const ExpiresInDefault: ExpiresInValue = {number: 30, interval: "day"};
|
||||
|
||||
const customExpiresFormat: DatePickerProps['format'] = value => {
|
||||
return formatDate(value)
|
||||
}
|
||||
const customExpiresFormat = (value: Date): string | null => {
|
||||
return formatDate(value);
|
||||
};
|
||||
|
||||
const customLastUsedFormat: DatePickerProps['format'] = value => {
|
||||
if (value.toString().startsWith("0001")) {
|
||||
return "never"
|
||||
const customLastUsedFormat = (value: Date): string | null => {
|
||||
if (value.getFullYear() === 1) {
|
||||
// 1st of Jan 0001
|
||||
return "never";
|
||||
}
|
||||
let ago = timeAgo(value.toString())
|
||||
if (!ago) {
|
||||
return "unused"
|
||||
}
|
||||
return ago
|
||||
}
|
||||
|
||||
interface FormSetupKey extends SetupKey {
|
||||
autoGroupNames: string[]
|
||||
}
|
||||
let ago = timeAgo(value.toString());
|
||||
if (!ago) return "unused";
|
||||
|
||||
return ago;
|
||||
};
|
||||
|
||||
const SetupKeyNew = () => {
|
||||
const {accessToken} = useOidcAccessToken()
|
||||
const dispatch = useDispatch()
|
||||
const setupNewKeyVisible = useSelector((state: RootState) => state.setupKey.setupNewKeyVisible)
|
||||
const setupKey = useSelector((state: RootState) => state.setupKey.setupKey)
|
||||
const savedSetupKey = useSelector((state: RootState) => state.setupKey.savedSetupKey)
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const [editName, setEditName] = useState(false)
|
||||
const inputNameRef = useRef<any>(null)
|
||||
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[])
|
||||
const [tagGroups, setTagGroups] = useState([] as string[])
|
||||
const {getTokenSilently} = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
const setupNewKeyVisible = useSelector((state: RootState) => state.setupKey.setupNewKeyVisible);
|
||||
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 [formSetupKey, setFormSetupKey] = useState({} as FormSetupKey)
|
||||
const [form] = Form.useForm()
|
||||
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);
|
||||
const isEditMode: boolean = !!formSetupKey.id;
|
||||
|
||||
useEffect(() => {
|
||||
if (editName) inputNameRef.current!.focus({
|
||||
cursor: 'end',
|
||||
});
|
||||
if (!editName) return;
|
||||
|
||||
inputNameRef.current!.focus({cursor: "end"});
|
||||
}, [editName]);
|
||||
|
||||
useEffect(() => {
|
||||
setTagGroups(groups?.filter(g => g.name != "All").map(g => g.name) || [])
|
||||
}, [groups])
|
||||
setTagGroups(groups?.filter((g) => g.name !== "All").map((g) => g.name) || []);
|
||||
}, [groups]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!setupKey) return
|
||||
if (!setupKey) return;
|
||||
|
||||
let allGroups = new Map<string, Group>();
|
||||
groups.forEach(g => {
|
||||
allGroups.set(g.id!, g)
|
||||
})
|
||||
|
||||
let formKeyGroups :string[] = []
|
||||
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)
|
||||
formKeyGroups = setupKey.auto_groups.filter((g) => allGroups.get(g)).map((g) => allGroups.get(g)!.name);
|
||||
}
|
||||
|
||||
const fSetupKey = {
|
||||
...setupKey,
|
||||
autoGroupNames: setupKey.auto_groups ? formKeyGroups : [],
|
||||
} as FormSetupKey
|
||||
setFormSetupKey(fSetupKey)
|
||||
form.setFieldsValue(fSetupKey)
|
||||
}, [setupKey])
|
||||
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 || '') || []
|
||||
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 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
|
||||
} as SetupKeyToSave
|
||||
}
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
let setupKeyToSave = createSetupKeyToSave()
|
||||
dispatch(setupKeyActions.saveSetupKey.request({
|
||||
getAccessTokenSilently: accessToken,
|
||||
payload: setupKeyToSave
|
||||
}))
|
||||
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,
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log('errorInfo', errorInfo)
|
||||
});
|
||||
);
|
||||
};
|
||||
|
||||
const setVisibleNewSetupKey = (status: boolean) => {
|
||||
form.resetFields();
|
||||
dispatch(setupKeyActions.setSetupNewKeyVisible(status));
|
||||
}
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedSetupKey.loading) return
|
||||
dispatch(setupKeyActions.setSetupKey({
|
||||
name: "",
|
||||
type: "reusable",
|
||||
key: "",
|
||||
last_used: "",
|
||||
expires: "",
|
||||
state: "valid",
|
||||
auto_groups: new Array()
|
||||
} as SetupKey))
|
||||
setFormSetupKey({} as FormSetupKey)
|
||||
setVisibleNewSetupKey(false)
|
||||
}
|
||||
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})
|
||||
}
|
||||
setFormSetupKey({...formSetupKey, ...data});
|
||||
};
|
||||
|
||||
const toggleEditName = (status: boolean) => {
|
||||
setEditName(status);
|
||||
}
|
||||
};
|
||||
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = []
|
||||
let hasSpaceNamed = [];
|
||||
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v)
|
||||
hasSpaceNamed.push(v);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (hasSpaceNamed.length) {
|
||||
return Promise.reject(new Error("Group names with just spaces are not allowed"))
|
||||
return Promise.reject(new Error("Group names with just spaces are not allowed"));
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const tagRender = (props: CustomTagProps) => {
|
||||
const {label, value, closable, onClose} = props;
|
||||
const {value, closable, onClose} = props;
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -187,30 +188,33 @@ const SetupKeyNew = () => {
|
||||
<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'} `
|
||||
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}}
|
||||
>
|
||||
<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'}}>
|
||||
<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>
|
||||
@@ -218,209 +222,350 @@ const SetupKeyNew = () => {
|
||||
<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"/>
|
||||
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 changesDetected = (): boolean => {
|
||||
return (
|
||||
formSetupKey.name == null ||
|
||||
formSetupKey.name !== setupKey.name ||
|
||||
groupsChanged() ||
|
||||
formSetupKey.usage_limit !== setupKey.usage_limit
|
||||
);
|
||||
};
|
||||
|
||||
const inputLabel = (text: any) => (
|
||||
<>
|
||||
<span>{text}</span>
|
||||
<Tag color="red">{formSetupKey.state}</Tag>
|
||||
</>
|
||||
)
|
||||
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;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{setupKey &&
|
||||
<Drawer
|
||||
forceRender={true}
|
||||
headerStyle={{display: "none"}}
|
||||
visible={setupNewKeyVisible}
|
||||
bodyStyle={{paddingBottom: 80}}
|
||||
onClose={onCancel}
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button disabled={savedSetupKey.loading} onClick={onCancel}>Cancel</Button>
|
||||
<Button type="primary" disabled={savedSetupKey.loading}
|
||||
onClick={handleFormSubmit}>{`${formSetupKey.id ? 'Save' : 'Create'}`}</Button>
|
||||
</Space>
|
||||
}
|
||||
<Modal
|
||||
style={{
|
||||
...{maxWidth: window.screen.availWidth <= 425 ? "90%" : "414px"},
|
||||
}}
|
||||
open={setupNewKeyVisible}
|
||||
onCancel={onCancel}
|
||||
footer={[
|
||||
<Container
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "end",
|
||||
padding: 0,
|
||||
}}
|
||||
key={0}
|
||||
>
|
||||
<Button onClick={onCancel}>Cancel</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
style={{
|
||||
height: "100%",
|
||||
fontSize: "14px",
|
||||
borderRadius: "2px",
|
||||
}}
|
||||
disabled={savedSetupKey.loading || !changesDetected()}
|
||||
onClick={handleFormSubmit}
|
||||
>
|
||||
{`${formSetupKey.id ? "Save" : "Create"} key`}
|
||||
</Button>
|
||||
</Container>,
|
||||
]}
|
||||
>
|
||||
<Container style={{textAlign: "start", marginLeft: "-15px", marginRight: "-15px"}}>
|
||||
<Paragraph
|
||||
style={{textAlign: "start", whiteSpace: "pre-line", fontSize: "22px"}}>
|
||||
{isEditMode ? "Setup key overview" : "Create setup key"}
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"}
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
marginTop: "-23px",
|
||||
paddingBottom: "15px",
|
||||
}}>
|
||||
{"Use this key to register new machines in your network"}
|
||||
</Paragraph>
|
||||
<Form
|
||||
layout="vertical"
|
||||
requiredMark={false}
|
||||
form={form}
|
||||
onValuesChange={onChange}
|
||||
initialValues={{
|
||||
expiresIn: ExpiresInDefault,
|
||||
usage_limit: 1,
|
||||
}}
|
||||
>
|
||||
<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 && setupKey.id &&
|
||||
<button type="button" aria-label="Close" className="ant-drawer-close"
|
||||
style={{paddingTop: 3}}
|
||||
onClick={onCancel}>
|
||||
<span role="img" aria-label="close"
|
||||
className="anticon anticon-close">
|
||||
<CloseOutlined size={16}/>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
{!editName && setupKey.id && formSetupKey.name ? (
|
||||
<div className={"access-control input-text ant-drawer-title"}
|
||||
onClick={() => toggleEditName(true)}>{formSetupKey.name ? formSetupKey.name : setupKey.name}
|
||||
<EditOutlined/></div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Name"
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please add a new name for this peer',
|
||||
whitespace: true
|
||||
}]}
|
||||
style={{display: 'flex'}}
|
||||
>
|
||||
<Input
|
||||
placeholder={setupKey.name}
|
||||
ref={inputNameRef}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Header>
|
||||
</Col>
|
||||
{setupKey.id && formSetupKey.name &&
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="key"
|
||||
label={<>
|
||||
<span style={{
|
||||
marginRight: "5px",
|
||||
}}>Key</span>
|
||||
<Tag
|
||||
color={formSetupKey.state === "valid" ? "green" : "red"}>{formSetupKey.state}</Tag>
|
||||
</>}
|
||||
>
|
||||
<Input
|
||||
disabled={true}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
}
|
||||
|
||||
{setupKey.id && formSetupKey.name &&
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="expires"
|
||||
label="Expires"
|
||||
tooltip="The expiration date of the key"
|
||||
>
|
||||
<DatePicker disabled={true} style={{width: '100%'}}
|
||||
format={customExpiresFormat}/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
}
|
||||
{setupKey.id && formSetupKey.name &&
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="last_used"
|
||||
label="Last Used"
|
||||
tooltip="The last time the key was used"
|
||||
>
|
||||
<DatePicker disabled={true} style={{width: '100%'}}
|
||||
format={customLastUsedFormat}/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
}
|
||||
<Row>
|
||||
{isEditMode ? (
|
||||
<></>
|
||||
) : (
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="type"
|
||||
label="Type"
|
||||
rules={[{required: true, message: 'Please enter key type'}]}
|
||||
style={{display: 'flex'}}
|
||||
<Paragraph style={{fontWeight: "bold"}}>
|
||||
Name
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>
|
||||
Set an easily identifiable name for your key
|
||||
</Paragraph>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
style={{marginBottom: "0px", marginTop: "10px"}}
|
||||
name="name"
|
||||
rules={[{required: true, message: "Please enter key name."}]}
|
||||
>
|
||||
{isEditMode ? (
|
||||
<Input
|
||||
key={"edit-name-input"}
|
||||
readOnly={!editName}
|
||||
placeholder={setupKey.name}
|
||||
autoComplete="off"
|
||||
ref={inputNameRef}
|
||||
suffix={<EditOutlined onClick={() => toggleEditName(true)}/>}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)}
|
||||
/>
|
||||
) : (
|
||||
<Input placeholder={`e.g. "AWS servers"`}/>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{isEditMode && (
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col span={24}>
|
||||
<Paragraph
|
||||
style={{whiteSpace: "pre-line", fontWeight: "bold", margin: 0}}
|
||||
>
|
||||
<Radio.Group style={{display: 'flex'}} disabled={setupKey.id}>
|
||||
<Space direction="vertical" style={{flex: 1}}>
|
||||
<List
|
||||
size="large"
|
||||
bordered
|
||||
>
|
||||
<List.Item>
|
||||
<Radio value={"reusable"}>
|
||||
<Space direction="vertical" size="small">
|
||||
<Text strong>Reusable</Text>
|
||||
<Text>This type of a setup key allows to enroll multiple
|
||||
machines</Text>
|
||||
</Space>
|
||||
</Radio>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<Radio value={"one-off"}>
|
||||
<Space direction="vertical" size="small">
|
||||
<Text strong>One-off</Text>
|
||||
<Text>This key can be used only once</Text>
|
||||
</Space>
|
||||
</Radio>
|
||||
</List.Item>
|
||||
</List>
|
||||
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="autoGroupNames"
|
||||
label="Auto-assigned groups"
|
||||
tooltip="Every peer enrolled with this key will be automatically added to these groups"
|
||||
rules={[{validator: selectValidator}]}
|
||||
>
|
||||
<Select mode="tags"
|
||||
style={{width: '100%'}}
|
||||
placeholder="Associate groups with the key"
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangeTags}
|
||||
dropdownRender={dropDownRender}
|
||||
// enabled only when we have a new key !setupkey.id or when the key is valid
|
||||
disabled={!(!setupKey.id || setupKey.valid)}
|
||||
Key
|
||||
<Tag
|
||||
color={`${formSetupKey.state === "valid" ? "green" : "red"}`}
|
||||
style={{marginLeft: "10px", borderRadius: "2px", fontWeight: "500"}}
|
||||
>
|
||||
{
|
||||
tagGroups.map(m =>
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Divider></Divider>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
|
||||
href="https://netbird.io/docs/overview/setup-keys"
|
||||
style={{color: 'rgb(07, 114, 128)'}}>Learn
|
||||
more about setup keys</Button>
|
||||
{formSetupKey.state}
|
||||
</Tag>
|
||||
</Paragraph>
|
||||
<Input
|
||||
style={{marginTop: "10px"}}
|
||||
disabled
|
||||
value={formSetupKey.key}
|
||||
suffix={<LockOutlined style={{color: "#BFBFBF"}}/>}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
)}
|
||||
{!isEditMode && (
|
||||
<Row
|
||||
style={{marginTop: "20px"}}
|
||||
justify={"space-between"}>
|
||||
<Col span={18}>
|
||||
<Paragraph
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
margin: 0,
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Reusable
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{whiteSpace: "pre-line", margin: 0}}>
|
||||
Use this type to enroll multiple peers
|
||||
</Paragraph>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Row justify={"end"}>
|
||||
<Form.Item
|
||||
name="reusable"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch
|
||||
onChange={(checked) => {
|
||||
setFormSetupKey({
|
||||
...formSetupKey,
|
||||
type: checked ? "reusable" : "one-off",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>)}
|
||||
|
||||
</Drawer>
|
||||
}
|
||||
</>
|
||||
{isEditMode && (
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Paragraph style={{whiteSpace: "pre-line", margin: 0, fontWeight: "bold"}}>
|
||||
{formSetupKey.type === "one-off" ? "One-off" : "Reusable"}
|
||||
</Paragraph>
|
||||
<Text type={"secondary"} style={{marginLeft: "5px"}}>key</Text>
|
||||
</Row>)}
|
||||
|
||||
<Row style={{marginTop: isEditMode? "20px" : "10px"}}>
|
||||
<Col span={24}>
|
||||
<Paragraph
|
||||
style={{whiteSpace: "pre-line", margin: 0, fontWeight: "bold"}}
|
||||
>
|
||||
{isEditMode ? "Available uses" : "Usage limit"}
|
||||
</Paragraph>
|
||||
</Col>
|
||||
|
||||
|
||||
{isEditMode && (
|
||||
<Col>
|
||||
<Input
|
||||
disabled
|
||||
value={formSetupKey.usage_limit - formSetupKey.used_times}
|
||||
suffix={<LockOutlined style={{color: "#BFBFBF"}}/>}
|
||||
style={{width: "104px", marginTop: "5px"}}
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
|
||||
{!isEditMode && (
|
||||
<Col>
|
||||
<Form.Item
|
||||
name="usage_limit"
|
||||
>
|
||||
<InputNumber
|
||||
type={"number"}
|
||||
style={{marginTop: "5px", width: "112px"}}
|
||||
disabled={setupKey.id || formSetupKey.type !== "reusable"}
|
||||
controls={false}
|
||||
min={0}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Paragraph type={"secondary"} style={{marginTop: "-18px", marginBottom: 0}}>
|
||||
For example, set to 30 if you want to enroll 30 peers
|
||||
</Paragraph>
|
||||
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
{!isEditMode && (
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col span={24}>
|
||||
<Paragraph
|
||||
style={{whiteSpace: "pre-line", margin: 0, fontWeight: "bold"}}
|
||||
>
|
||||
Expires in
|
||||
</Paragraph>
|
||||
</Col>
|
||||
<Col>
|
||||
<Form.Item
|
||||
name="expiresIn"
|
||||
rules={[{required: true, message: "Please enter expiration date"}]}
|
||||
>
|
||||
<InputNumber defaultValue={7} placeholder={`2`} type="number" addonAfter=" Days"
|
||||
style={{width: "160px", marginTop: "5px"}}/>
|
||||
</Form.Item>
|
||||
<Paragraph type={"secondary"} style={{marginTop: "-18px", marginBottom: 0}}>
|
||||
Should be between 1 and 180 days
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>)}
|
||||
|
||||
{isEditMode && (
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
|
||||
<Container style={{width: "100%", padding: 0, margin: 0}}>
|
||||
<Row>
|
||||
<Col span={11}>
|
||||
<Paragraph style={{margin: 0, fontWeight: "bold"}}>
|
||||
Expires
|
||||
</Paragraph>
|
||||
<Row>
|
||||
<Input
|
||||
style={{marginTop: "5px"}}
|
||||
disabled
|
||||
suffix={<LockOutlined style={{color: "#BFBFBF"}}/>}
|
||||
value={customExpiresFormat(new Date(formSetupKey.expires))!}
|
||||
/>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={11} offset={1}>
|
||||
<Paragraph style={{margin: 0, fontWeight: "bold"}}>
|
||||
Last used
|
||||
</Paragraph>
|
||||
<Row>
|
||||
<Input
|
||||
disabled
|
||||
style={{marginTop: "5px"}}
|
||||
suffix={<LockOutlined style={{color: "#BFBFBF"}}/>}
|
||||
value={customLastUsedFormat(new Date(formSetupKey.last_used))!}
|
||||
/>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
</Row>)}
|
||||
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<Col span={24}>
|
||||
<Paragraph
|
||||
style={{whiteSpace: "pre-line", margin: 0, fontWeight: "bold"}}
|
||||
>
|
||||
Auto-assigned groups
|
||||
</Paragraph>
|
||||
{isEditMode ? (
|
||||
<></>
|
||||
) : (
|
||||
<Text type={"secondary"}>
|
||||
These groups will be automatically assigned to peers enrolled with this key
|
||||
</Text>
|
||||
)}
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
style={{marginTop: "5px", marginBottom: 0}}
|
||||
name="autoGroupNames"
|
||||
rules={[{validator: selectValidator}]}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{width: "100%"}}
|
||||
placeholder="Associate groups with the key"
|
||||
tagRender={tagRender}
|
||||
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>
|
||||
</Row>
|
||||
<Row style={{marginTop: "40px", marginBottom: "28px"}}>
|
||||
<Text type={"secondary"}>
|
||||
Learn more about
|
||||
<a target="_blank" rel="noreferrer" href="https://netbird.io/docs/overview/setup-keys">
|
||||
{" "}
|
||||
setup keys
|
||||
</a>
|
||||
</Text>
|
||||
</Row>
|
||||
</Form>
|
||||
</Container>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
;
|
||||
};
|
||||
|
||||
export default SetupKeyNew
|
||||
export default SetupKeyNew;
|
||||
525
src/components/UserEdit.tsx
Normal file
525
src/components/UserEdit.tsx
Normal file
@@ -0,0 +1,525 @@
|
||||
import {
|
||||
Badge,
|
||||
Breadcrumb,
|
||||
Button, Card,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
List, Modal,
|
||||
Row,
|
||||
Select,
|
||||
Skeleton,
|
||||
Space, 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 {Container} from "./Container";
|
||||
import Column from "antd/lib/table/Column";
|
||||
import {useOidcUser} from "@axa-fr/react-oidc";
|
||||
|
||||
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 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 [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
|
||||
} as UserToSave
|
||||
}
|
||||
|
||||
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 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 dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{margin: '8px 0'}}/>
|
||||
<Row style={{padding: '0 8px 4px'}}>
|
||||
<Col flex="auto">
|
||||
<span style={{color: "#9CA3AF"}}>Add new group by pressing "Enter"</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
|
||||
fill="#9CA3AF"/>
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
|
||||
const optionRender = (label: string) => {
|
||||
let peersCount = ''
|
||||
const g = groups.find(_g => _g.name === label)
|
||||
if (g) peersCount = ` - ${g.peers_count || 0} ${(!g.peers_count || parseInt(g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{label}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const 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: "Delete token \"" + token.name + "\"",
|
||||
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,
|
||||
autoGroupsNames: currentGroups,
|
||||
})
|
||||
}
|
||||
}, [form, user, currentGroups])
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<Text onClick={() => onBreadcrumbUsersClick("Users")}>
|
||||
Users
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: (
|
||||
<Text onClick={() => onBreadcrumbUsersClick("Service Users")}>
|
||||
Service Users
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const isUserAdmin = (userId: string): boolean => {
|
||||
return users.find(u => u.id === userId)?.role === "admin"
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{paddingTop: "13px"}}>
|
||||
<Breadcrumb style={{marginBottom: "30px"}}
|
||||
items={[
|
||||
{
|
||||
title: <a href = "" onClick={() => onBreadcrumbUsersClick("Users")}>All Users</a>,
|
||||
},
|
||||
{
|
||||
title: <a href= "" onClick={() => onBreadcrumbUsersClick(tab)}>{tab}</a>,
|
||||
// menu: { items: menuItems },
|
||||
},
|
||||
{
|
||||
title: user.name,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Card
|
||||
bordered={true}
|
||||
title={user.name}
|
||||
loading={loading}
|
||||
style={{marginBottom: "7px"}}
|
||||
>
|
||||
<div style={{maxWidth: "800px"}}>
|
||||
<Form layout="vertical" hideRequiredMark form={form}
|
||||
initialValues={{
|
||||
name: formUser.name,
|
||||
role: formUser.role,
|
||||
email: formUser.email,
|
||||
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"}}
|
||||
>
|
||||
<Input
|
||||
disabled={user.id}
|
||||
value={formUser.email}
|
||||
style={{color: "#5a5c5a"}}
|
||||
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={{}}>Role</Text>}
|
||||
style={{marginRight: "50px"}}
|
||||
>
|
||||
<Select style={{width: '100%'}}
|
||||
disabled={user.is_current}>
|
||||
<Option value="admin">admin</Option>
|
||||
<Option value="user">user</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
{!user.is_service_user && <Row style={{paddingBottom: "15px"}}>
|
||||
<Col span={9}>
|
||||
<Form.Item
|
||||
name="autoGroupsNames"
|
||||
label={<Text style={{}}>Auto-assigned groups</Text>}
|
||||
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}
|
||||
dropdownRender={dropDownRender}
|
||||
disabled={oidcUser && !isUserAdmin(oidcUser.sub)}
|
||||
>
|
||||
{
|
||||
tagGroups.map(m =>
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</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: "16px", fontWeight: "bold"}}>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,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 apt-key add -`,
|
||||
`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
1
src/components/icons/docker_icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" ?><svg data-name="Layer 1" id="Layer_1" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M507,211.16c-1.42-1.19-14.25-10.94-41.79-10.94a132.55,132.55,0,0,0-21.61,1.9c-5.22-36.4-35.38-54-36.57-55l-7.36-4.28-4.75,6.9a101.65,101.65,0,0,0-13.06,30.45c-5,20.7-1.9,40.2,8.55,56.85-12.59,7.14-33,8.8-37.28,9H15.94A15.93,15.93,0,0,0,0,262.07,241.25,241.25,0,0,0,14.75,348.9C26.39,379.35,43.72,402,66,415.74,91.22,431.2,132.3,440,178.6,440a344.23,344.23,0,0,0,62.45-5.71,257.44,257.44,0,0,0,81.69-29.73,223.55,223.55,0,0,0,55.57-45.67c26.83-30.21,42.74-64,54.38-94h4.75c29.21,0,47.26-11.66,57.23-21.65a63.31,63.31,0,0,0,15.2-22.36l2.14-6.18Z"/><path d="M47.29,236.37H92.4a4,4,0,0,0,4-4h0V191.89a4,4,0,0,0-4-4H47.29a4,4,0,0,0-4,4h0v40.44a4.16,4.16,0,0,0,4,4h0"/><path d="M109.5,236.37h45.12a4,4,0,0,0,4-4h0V191.89a4,4,0,0,0-4-4H109.5a4,4,0,0,0-4,4v40.44a4.16,4.16,0,0,0,4,4"/><path d="M172.9,236.37H218a4,4,0,0,0,4-4h0V191.89a4,4,0,0,0-4-4H172.9a4,4,0,0,0-4,4h0v40.44a3.87,3.87,0,0,0,4,4h0"/><path d="M235.36,236.37h45.12a4,4,0,0,0,4-4V191.89a4,4,0,0,0-4-4H235.36a4,4,0,0,0-4,4h0v40.44a4,4,0,0,0,4,4h0"/><path d="M109.5,178.57h45.12a4.16,4.16,0,0,0,4-4V134.09a4,4,0,0,0-4-4H109.5a4,4,0,0,0-4,4v40.44a4.34,4.34,0,0,0,4,4"/><path d="M172.9,178.57H218a4.16,4.16,0,0,0,4-4V134.09a4,4,0,0,0-4-4H172.9a4,4,0,0,0-4,4h0v40.44a4,4,0,0,0,4,4"/><path d="M235.36,178.57h45.12a4.16,4.16,0,0,0,4-4V134.09a4.16,4.16,0,0,0-4-4H235.36a4,4,0,0,0-4,4h0v40.44a4.16,4.16,0,0,0,4,4"/><path d="M235.36,120.53h45.12a4,4,0,0,0,4-4V76a4.16,4.16,0,0,0-4-4H235.36a4,4,0,0,0-4,4h0v40.44a4.17,4.17,0,0,0,4,4"/><path d="M298.28,236.37H343.4a4,4,0,0,0,4-4V191.89a4,4,0,0,0-4-4H298.28a4,4,0,0,0-4,4h0v40.44a4.16,4.16,0,0,0,4,4"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
src/components/icons/terminal_icon.svg
Normal file
1
src/components/icons/terminal_icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" ?><svg height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg"><title/><path d="M432,32H80A64.07,64.07,0,0,0,16,96V416a64.07,64.07,0,0,0,64,64H432a64.07,64.07,0,0,0,64-64V96A64.07,64.07,0,0,0,432,32ZM96,256a16,16,0,0,1-10-28.49L150.39,176,86,124.49a16,16,0,1,1,20-25l80,64a16,16,0,0,1,0,25l-80,64A16,16,0,0,1,96,256Zm160,0H192a16,16,0,0,1,0-32h64a16,16,0,0,1,0,32Z"/></svg>
|
||||
|
After Width: | Height: | Size: 419 B |
220
src/components/popups/AddPATPopup.tsx
Normal file
220
src/components/popups/AddPATPopup.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
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])
|
||||
|
||||
useEffect(() => {
|
||||
const keyDownHandler = (event: any) => {
|
||||
console.log('User pressed: ', event.key);
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
handleFormSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', keyDownHandler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', keyDownHandler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<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: "22px"}}>
|
||||
{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: "bold", 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: "bold", 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://netbird.io/docs/overview/personal-access-tokens">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
|
||||
292
src/components/popups/AddServiceUserPopup.tsx
Normal file
292
src/components/popups/AddServiceUserPopup.tsx
Normal file
@@ -0,0 +1,292 @@
|
||||
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: "22px"}}>
|
||||
{"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: "bold", 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: "bold", 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
|
||||
315
src/components/popups/InviteUserPopup.tsx
Normal file
315
src/components/popups/InviteUserPopup.tsx
Normal file
@@ -0,0 +1,315 @@
|
||||
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";
|
||||
|
||||
const {Title, Text, Paragraph} = Typography;
|
||||
const {Option} = Select;
|
||||
|
||||
const InviteUserPopup = () => {
|
||||
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 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={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: "22px", fontWeight: "500"}}>
|
||||
{"Invite user"}
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"}
|
||||
style={{
|
||||
textAlign: "start",
|
||||
whiteSpace: "pre-line",
|
||||
fontSize: "14px",
|
||||
marginTop: "-23px",
|
||||
paddingBottom: "25px",
|
||||
}}>
|
||||
{"Invite a user to your network and set their permissions."}
|
||||
</Paragraph>
|
||||
<Form layout="vertical" hideRequiredMark form={form}
|
||||
initialValues={{
|
||||
["role"]: "user"
|
||||
}}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{fontWeight: "bold", 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: "bold", 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: "bold", 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: "bold", 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={tagRender}
|
||||
onChange={handleChangeTags}
|
||||
dropdownRender={dropDownRender}
|
||||
>
|
||||
{
|
||||
tagGroups.map(m =>
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{/*<Col span={24}>*/}
|
||||
{/* <Button icon={<QuestionCircleFilled/>} type="link" target="_blank" disabled={true} style={{marginTop: "20px", marginBottom: "20px"}}*/}
|
||||
{/* href="https://netbird.io/docs/overview/personal-access-tokens">Learn more about user</Button>*/}
|
||||
{/*</Col>*/}
|
||||
</Row>
|
||||
</Form>
|
||||
</Container>
|
||||
</Modal>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default InviteUserPopup
|
||||
100
src/components/popups/addpeer/addpeer/AddPeerPopup.tsx
Normal file
100
src/components/popups/addpeer/addpeer/AddPeerPopup.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React, {useState} from 'react';
|
||||
|
||||
import {Tabs, TabsProps} from "antd";
|
||||
import Icon, {AndroidFilled, AppleFilled, WindowsFilled} from "@ant-design/icons";
|
||||
import {ReactComponent as LinuxSVG} from "../../../icons/terminal_icon.svg";
|
||||
import UbuntuTab from "./UbuntuTab";
|
||||
import {ReactComponent as DockerSVG} from "../../../icons/docker_icon.svg";
|
||||
import Paragraph from "antd/lib/typography/Paragraph";
|
||||
import WindowsTab from "./WindowsTab";
|
||||
import MacTab from "./MacTab";
|
||||
import Link from "antd/lib/typography/Link";
|
||||
import DockerTab from "./DockerTab";
|
||||
|
||||
type Props = {
|
||||
greeting?: string;
|
||||
headline: string;
|
||||
};
|
||||
|
||||
const detectOSTab = () => {
|
||||
let os = 1;
|
||||
if (navigator.userAgent.indexOf("Win") !== -1) os = 2;
|
||||
if (navigator.userAgent.indexOf("Mac") !== -1) os = 3;
|
||||
if (navigator.userAgent.indexOf("X11") !== -1) os = 1;
|
||||
if (navigator.userAgent.indexOf("Linux") !== -1) os = 1
|
||||
return os
|
||||
}
|
||||
|
||||
export const AddPeerPopup: React.FC<Props> = ({
|
||||
greeting,
|
||||
headline,
|
||||
}) => {
|
||||
|
||||
const [openTab, setOpenTab] = useState(detectOSTab);
|
||||
|
||||
const [width, setWidth] = useState<number>(window.innerWidth);
|
||||
const isMobile = width <= 768;
|
||||
|
||||
const items: TabsProps['items'] = [
|
||||
{
|
||||
key: "1",
|
||||
label: <span><Icon component={LinuxSVG}/>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: <></>,
|
||||
},*/
|
||||
{
|
||||
key: "5",
|
||||
label: <span><Icon component={DockerSVG}/>Docker</span>,
|
||||
children: <DockerTab/>,
|
||||
}
|
||||
];
|
||||
|
||||
return <>
|
||||
{greeting && <Paragraph
|
||||
style={{textAlign: "center", whiteSpace: "pre-line", fontSize: "2em", marginBottom: -10}}>
|
||||
{greeting}
|
||||
</Paragraph>}
|
||||
<Paragraph
|
||||
style={{textAlign: "center", whiteSpace: "pre-line", fontSize: "2em"}}>
|
||||
{headline}
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-15px",
|
||||
textAlign: "center",
|
||||
whiteSpace: "pre-line",
|
||||
}}>
|
||||
To get started install NetBird and log in using your {"\n"} email account.
|
||||
</Paragraph>
|
||||
|
||||
<Tabs centered={!isMobile}
|
||||
defaultActiveKey={openTab.toString()} tabPosition="top" animated={{inkBar: true, tabPane: false}}
|
||||
items={items}/>
|
||||
<Paragraph type={"secondary"}
|
||||
style={{
|
||||
marginTop: "15px",
|
||||
}}>
|
||||
After that you should be connected. Add more devices to your network or manage your existing devices in the
|
||||
admin panel.
|
||||
If you have further questions check out our {<Link target="_blank"
|
||||
href={"https://netbird.io/docs/getting-started/installation"}>installation
|
||||
guide</Link>}
|
||||
</Paragraph>
|
||||
</>
|
||||
}
|
||||
|
||||
export default AddPeerPopup
|
||||
63
src/components/popups/addpeer/addpeer/DockerTab.tsx
Normal file
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://netbird.io/docs/getting-started/installation#running-netbird-in-docker" target="_blank">Running NetBird in Docker</Link>
|
||||
),
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
} as StepCommand
|
||||
])
|
||||
|
||||
return (
|
||||
<div style={{marginTop: 10}}>
|
||||
{/*<Text style={{fontWeight: "bold"}}>
|
||||
Run in Docker
|
||||
</Text>
|
||||
<div style={{fontSize: ".85em", marginTop: 5, marginBottom: 25}}>
|
||||
<SyntaxHighlighter language="bash">
|
||||
{formatDockerCommand()}
|
||||
</SyntaxHighlighter>
|
||||
</div>*/}
|
||||
<Text style={{fontWeight: "bold"}}>
|
||||
Install on Ubuntu
|
||||
</Text>
|
||||
<div style={{marginTop: 5}}>
|
||||
<TabSteps stepsItems={steps}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default DockerTab
|
||||
113
src/components/popups/addpeer/addpeer/MacTab.tsx
Normal file
113
src/components/popups/addpeer/addpeer/MacTab.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import React, {useState} from 'react';
|
||||
|
||||
import {Button, 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";
|
||||
const { Panel } = Collapse;
|
||||
|
||||
const {Text} = Typography;
|
||||
|
||||
export const LinuxTab = () => {
|
||||
|
||||
const [quickSteps, setQuickSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: 'Download and run installer:',
|
||||
commands: (
|
||||
<Button style={{marginTop: "5px"}} type="primary" href="https://pkgs.netbird.io/windows/x64"
|
||||
target="_blank">Download NetBird</Button>
|
||||
),
|
||||
copied: false
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 2,
|
||||
title: 'Click on "Connect" from the NetBird icon in your system tray',
|
||||
commands: '',
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
title: 'Sign up using your email address',
|
||||
commands: '',
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
}
|
||||
])
|
||||
|
||||
const [steps, setSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: 'Download and install Homebrew',
|
||||
commands: (
|
||||
<Button style={{marginTop: "5px"}} type="primary" href="https://brew.sh/" target="_blank">Download
|
||||
Brew</Button>
|
||||
),
|
||||
copied: false
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 2,
|
||||
title: 'Install NetBird:',
|
||||
commands: [
|
||||
`# for CLI only`,
|
||||
`brew install netbirdio/tap/netbird`,
|
||||
`# for GUI package`,
|
||||
`brew install --cask netbirdio/tap/netbird-ui`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 3,
|
||||
title: 'Start NetBird daemon:',
|
||||
commands: [
|
||||
`sudo netbird service install`,
|
||||
`sudo netbird service start`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 4,
|
||||
title: 'Run NetBird and log in the browser:',
|
||||
commands: formatNetBirdUP(),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand
|
||||
])
|
||||
|
||||
return (
|
||||
<div style={{marginTop: 10}}>
|
||||
<Text style={{fontWeight: "bold"}}>
|
||||
Install with one command
|
||||
</Text>
|
||||
<div style={{fontSize: ".85em", marginTop: 5, marginBottom: 25}}>
|
||||
<SyntaxHighlighter language="bash">
|
||||
curl -fsSL https://pkgs.netbird.io/install.sh | sh
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
<Text style={{fontWeight: "bold"}}>
|
||||
Or install manually with HomeBrew
|
||||
</Text>
|
||||
<div style={{marginTop: 5}}>
|
||||
<TabSteps stepsItems={steps}/>
|
||||
</div>
|
||||
</div>
|
||||
/*<div style={{marginTop: 5}}>
|
||||
<TabSteps stepsItems={quickSteps}/>
|
||||
</div>*/
|
||||
/*<div style={{marginTop: 10}}>
|
||||
<Text style={{fontWeight: "bold"}}>
|
||||
Install on macOS with Homebrew
|
||||
</Text>
|
||||
<div style={{marginTop: 5}}>
|
||||
<TabSteps stepsItems={steps}/>
|
||||
</div>
|
||||
</div>*/
|
||||
)
|
||||
}
|
||||
|
||||
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
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
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
|
||||
@@ -7,5 +7,8 @@
|
||||
|
||||
"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",
|
||||
"tokenSource": "$NETBIRD_TOKEN_SOURCE"
|
||||
}
|
||||
@@ -7,7 +7,24 @@ if (process.env.NODE_ENV !== 'production') {
|
||||
configJson = require("./config.json");
|
||||
}
|
||||
|
||||
const defaultRedirectURI = '/#callback';
|
||||
const defaultSilentRedirectURI = '/#silent-callback'
|
||||
const defaultTokenSource = "accessToken"
|
||||
export function getConfig() {
|
||||
let redirectURI = defaultRedirectURI
|
||||
if (configJson.redirectURI) {
|
||||
redirectURI = configJson.redirectURI
|
||||
}
|
||||
|
||||
let silentRedirectURI = defaultSilentRedirectURI
|
||||
if (configJson.silentRedirectURI) {
|
||||
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
|
||||
@@ -16,7 +33,10 @@ export function getConfig() {
|
||||
scopesSupported: configJson.authScopesSupported,
|
||||
apiOrigin: configJson.apiOrigin,
|
||||
grpcApiOrigin: configJson.grpcApiOrigin,
|
||||
latestVersion: configJson.latestVersion,
|
||||
audience: configJson.authAudience,
|
||||
hotjarTrackID: configJson.hotjarTrackID,
|
||||
redirectURI: redirectURI,
|
||||
silentRedirectURI: silentRedirectURI,
|
||||
tokenSource: tokenSource,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
@import '~antd/dist/antd.css';
|
||||
|
||||
/*@tailwind base;*/
|
||||
/*@tailwind components;*/
|
||||
/*@tailwind utilities;*/
|
||||
@import 'antd/dist/reset.css';
|
||||
|
||||
body {
|
||||
font-size: 16px;
|
||||
@@ -92,20 +88,6 @@ body {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.ant-steps-item-tail::after {
|
||||
background-color: #1890ff !important;
|
||||
}
|
||||
|
||||
.ant-steps-item-icon {
|
||||
background: #1890ff !important;
|
||||
color: #ffffff !important;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.ant-steps-icon {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.nb-code {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
@@ -135,4 +117,38 @@ 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;
|
||||
}
|
||||
@@ -26,9 +26,9 @@ const auth0AuthorityConfig: AuthorityConfiguration = {
|
||||
const providerConfig = {
|
||||
authority: config.authority,
|
||||
client_id: config.clientId,
|
||||
redirect_uri: window.location.origin + '/#callback',
|
||||
redirect_uri: window.location.origin + config.redirectURI,
|
||||
refresh_time_before_tokens_expiration_in_second: 30,
|
||||
silent_redirect_uri: window.location.origin + '/#silent-callback',
|
||||
silent_redirect_uri: window.location.origin + config.silentRedirectURI,
|
||||
scope: config.scopesSupported,
|
||||
// disabling service worker
|
||||
// service_worker_relative_url:'/OidcServiceWorker.js',
|
||||
@@ -41,7 +41,7 @@ const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
|
||||
const loadingComponent = () => <Loading padding="3em" width="50px" height="50px"/>
|
||||
const loadingComponent = () => <Loading padding="3em" width={50} height={50}/>
|
||||
|
||||
root.render(
|
||||
<OidcProvider
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import axios, {AxiosError} from 'axios';
|
||||
import axios from 'axios';
|
||||
|
||||
import {ApiRequestParams, ApiResponse, ApiError} from './types';
|
||||
import {ApiError, ApiRequestParams, ApiResponse} from './types';
|
||||
import {headersFactory, RequestHeader} from './header-factory';
|
||||
|
||||
/*axios.interceptors.response.use(undefined, err => {
|
||||
let res = err.response;
|
||||
if (res.status === 401) {
|
||||
}
|
||||
})*/
|
||||
|
||||
async function apiRequest<T>(params: ApiRequestParams): Promise<ApiResponse<T>> {
|
||||
const data = params.data ? (params.data as any).payload : undefined;
|
||||
const url = `${params.urlBase}${params.url}`;
|
||||
@@ -16,18 +10,20 @@ async function apiRequest<T>(params: ApiRequestParams): Promise<ApiResponse<T>>
|
||||
const extraHeaders = params.extraHeaders || {};
|
||||
const headers = await headersFactory((params.data as any).getAccessTokenSilently);
|
||||
|
||||
const builtHeader: RequestHeader = { ...headers, ...extraHeaders };
|
||||
const builtHeader: RequestHeader = {...headers, ...extraHeaders};
|
||||
|
||||
let response;
|
||||
let error:ApiError = {
|
||||
let error: ApiError = {
|
||||
code: '-1',
|
||||
message: '',
|
||||
data: null,
|
||||
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',
|
||||
@@ -39,12 +35,15 @@ async function apiRequest<T>(params: ApiRequestParams): Promise<ApiResponse<T>>
|
||||
let old = error.message
|
||||
error.message = old + ". Please refresh the page if the issue continues."
|
||||
error.code = 'ERR_UNAUTHORIZED'
|
||||
} else if (error.statusCode === 403) {
|
||||
error.code = 'ERR_FORBIDDEN'
|
||||
console.log(error)
|
||||
}
|
||||
console.log(error)
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { statusCode: response ? response.status : error.statusCode, body: response ? response.data : error };
|
||||
return {statusCode: response ? response.status : error.statusCode, body: response ? response.data : error};
|
||||
}
|
||||
|
||||
export { apiRequest };
|
||||
export {apiRequest};
|
||||
|
||||
@@ -9,11 +9,7 @@ const headersFactory = async (getAccessTokenSilently:any): Promise<RequestHeader
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
|
||||
//const token = await getLocalItem<string>(StorageKey.token);
|
||||
//const token = ''
|
||||
// const token = await getAccessTokenSilently()
|
||||
const token = getAccessTokenSilently
|
||||
|
||||
const token = await getAccessTokenSilently() as string
|
||||
if (token) {
|
||||
headers.authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
@@ -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
23
src/store/account/actions.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {ActionType, createAction, createAsyncAction} from 'typesafe-actions';
|
||||
import {Account} from './types';
|
||||
import {ApiError, ChangeResponse, RequestPayload} from '../../services/api-client/types';
|
||||
|
||||
const actions = {
|
||||
getAccounts: createAsyncAction(
|
||||
'GET_ACCOUNTS_REQUEST',
|
||||
'GET_ACCOUNTS_SUCCESS',
|
||||
'GET_ACCOUNTS_FAILURE',
|
||||
)<RequestPayload<null>, Account[], ApiError>(),
|
||||
|
||||
updateAccount: createAsyncAction(
|
||||
'UPDATE_ACCOUNT',
|
||||
'UPDATE_ACCOUNT_SUCCESS',
|
||||
'UPDATE_ACCOUNT_FAILURE',
|
||||
)<RequestPayload<Account>, ChangeResponse<Account | null>, ChangeResponse<Account | null>>(),
|
||||
setUpdateAccount: createAction('SET_UPDATED_ACCOUNT')<ChangeResponse<Account | null>>(),
|
||||
resetUpdateAccount: createAction('RESET_UPDATED_ACCOUNT')<null>(),
|
||||
};
|
||||
|
||||
|
||||
export type ActionTypes = ActionType<typeof actions>;
|
||||
export default actions;
|
||||
7
src/store/account/index.ts
Normal file
7
src/store/account/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import actions, { ActionTypes as _actionTypes } from './actions';
|
||||
import reducer from './reducer';
|
||||
import sagas from './sagas';
|
||||
|
||||
export type ActionTypes = _actionTypes;
|
||||
|
||||
export { actions, reducer, sagas };
|
||||
54
src/store/account/reducer.ts
Normal file
54
src/store/account/reducer.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { createReducer } from 'typesafe-actions';
|
||||
import { combineReducers } from 'redux';
|
||||
import { Account } from './types';
|
||||
import actions, { ActionTypes } from './actions';
|
||||
import {ApiError, ChangeResponse} from "../../services/api-client/types";
|
||||
|
||||
type StateType = Readonly<{
|
||||
data: Account[] | null;
|
||||
loading: boolean;
|
||||
failed: ApiError | null;
|
||||
savedAccount: ChangeResponse<Account | null>;
|
||||
}>;
|
||||
|
||||
const initialState: StateType = {
|
||||
data: [],
|
||||
loading: false,
|
||||
failed: null,
|
||||
savedAccount: <ChangeResponse<Account | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
};
|
||||
|
||||
const data = createReducer<Account[], ActionTypes>(initialState.data as Account[])
|
||||
.handleAction(actions.getAccounts.success,(_, action) => action.payload)
|
||||
.handleAction(actions.getAccounts.failure, () => []);
|
||||
|
||||
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
|
||||
.handleAction(actions.getAccounts.request, () => true)
|
||||
.handleAction(actions.getAccounts.success, () => false)
|
||||
.handleAction(actions.getAccounts.failure, () => false);
|
||||
|
||||
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
|
||||
.handleAction(actions.getAccounts.request, () => null)
|
||||
.handleAction(actions.getAccounts.success, () => null)
|
||||
.handleAction(actions.getAccounts.failure, (store, action) => action.payload);
|
||||
|
||||
const updatedAccount = createReducer<ChangeResponse<Account | null>, ActionTypes>(initialState.savedAccount)
|
||||
.handleAction(actions.updateAccount.request, () => initialState.savedAccount)
|
||||
.handleAction(actions.updateAccount.success, (store, action) => action.payload)
|
||||
.handleAction(actions.updateAccount.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setUpdateAccount, (store, action) => action.payload)
|
||||
.handleAction(actions.resetUpdateAccount, () => initialState.savedAccount)
|
||||
|
||||
|
||||
export default combineReducers({
|
||||
data,
|
||||
loading,
|
||||
failed,
|
||||
updatedAccount
|
||||
});
|
||||
64
src/store/account/sagas.ts
Normal file
64
src/store/account/sagas.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import {all, call, put, takeLatest} from 'redux-saga/effects';
|
||||
import {ApiError, ApiResponse, ChangeResponse} from '../../services/api-client/types';
|
||||
import {Account} from './types'
|
||||
import service from './service';
|
||||
import actions from './actions';
|
||||
|
||||
export function* getAccounts(action: ReturnType<typeof actions.getAccounts.request>): Generator {
|
||||
try {
|
||||
const effect = yield call(service.getAccounts, action.payload);
|
||||
const response = effect as ApiResponse<Account[]>;
|
||||
|
||||
yield put(actions.getAccounts.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getAccounts.failure(err as ApiError));
|
||||
}
|
||||
}
|
||||
|
||||
export function* updateAccount(action: ReturnType<typeof actions.updateAccount.request>): Generator {
|
||||
try {
|
||||
yield put(actions.setUpdateAccount({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
}))
|
||||
|
||||
const account = action.payload.payload
|
||||
|
||||
const payloadToSave = {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: account
|
||||
}
|
||||
|
||||
const effect = yield call(service.updateAccount, payloadToSave)
|
||||
const response = effect as ApiResponse<Account>;
|
||||
|
||||
yield put(actions.updateAccount.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as ChangeResponse<Account | null>));
|
||||
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
yield put(actions.updateAccount.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: true,
|
||||
error: err as ApiError,
|
||||
data: null
|
||||
} as ChangeResponse<Account | null>));
|
||||
}
|
||||
}
|
||||
|
||||
export default function* sagas(): Generator {
|
||||
yield all([
|
||||
takeLatest(actions.getAccounts.request, getAccounts),
|
||||
takeLatest(actions.updateAccount.request, updateAccount),
|
||||
]);
|
||||
}
|
||||
|
||||
19
src/store/account/service.ts
Normal file
19
src/store/account/service.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
|
||||
import { apiClient } from '../../services/api-client';
|
||||
import {Account} from './types';
|
||||
|
||||
export default {
|
||||
async getAccounts(payload:RequestPayload<null>): Promise<ApiResponse<Account[]>> {
|
||||
return apiClient.get<Account[]>(
|
||||
`/api/accounts`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async updateAccount(payload:RequestPayload<Account>): Promise<ApiResponse<Account>> {
|
||||
const id = payload.payload.id
|
||||
return apiClient.put<Account>(
|
||||
`/api/accounts/${id}`,
|
||||
payload
|
||||
);
|
||||
}
|
||||
};
|
||||
11
src/store/account/types.ts
Normal file
11
src/store/account/types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import {ExpiresInValue} from "../../views/ExpiresInInput";
|
||||
|
||||
export interface Account {
|
||||
id: string;
|
||||
settings: { peer_login_expiration_enabled: boolean, peer_login_expiration: number}
|
||||
}
|
||||
|
||||
export interface FormAccount extends Account {
|
||||
peer_login_expiration_enabled: boolean
|
||||
peer_login_expiration_formatted : ExpiresInValue
|
||||
}
|
||||
26
src/store/dns-settings/actions.ts
Normal file
26
src/store/dns-settings/actions.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
|
||||
import {DNSSettings, DNSSettingsToSave} from './types';
|
||||
import {ApiError, CreateResponse, RequestPayload} from '../../services/api-client/types';
|
||||
|
||||
const actions = {
|
||||
getDNSSettings: createAsyncAction(
|
||||
'GET_DNSSettings_REQUEST',
|
||||
'GET_DNSSettings_SUCCESS',
|
||||
'GET_DNSSettings_FAILURE',
|
||||
)<RequestPayload<null>, DNSSettings, ApiError>(),
|
||||
|
||||
saveDNSSettings: createAsyncAction(
|
||||
'SAVE_DNSSettings_REQUEST',
|
||||
'SAVE_DNSSettings_SUCCESS',
|
||||
'SAVE_DNSSettings_FAILURE',
|
||||
)<RequestPayload<DNSSettingsToSave>, CreateResponse<DNSSettings | null>, CreateResponse<DNSSettings | null>>(),
|
||||
setSavedDNSSettings: createAction('SET_CREATE_DNSSettings')<CreateResponse<DNSSettings | null>>(),
|
||||
resetSavedDNSSettings: createAction('RESET_CREATE_DNSSettings')<null>(),
|
||||
|
||||
setDNSSettings: createAction('SET_DNSSettings')<DNSSettings>(),
|
||||
setSetupNewDNSSettingsVisible: createAction('SET_SETUP_NEW_DNSSettings_VISIBLE')<boolean>(),
|
||||
setSetupNewDNSSettingsHA: createAction('SET_SETUP_NEW_DNSSettings_HA')<boolean>()
|
||||
};
|
||||
|
||||
export type ActionTypes = ActionType<typeof actions>;
|
||||
export default actions;
|
||||
7
src/store/dns-settings/index.ts
Normal file
7
src/store/dns-settings/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import actions, { ActionTypes as _actionTypes } from './actions';
|
||||
import reducer from './reducer';
|
||||
import sagas from './sagas';
|
||||
|
||||
export type ActionTypes = _actionTypes;
|
||||
|
||||
export { actions, reducer, sagas };
|
||||
79
src/store/dns-settings/reducer.ts
Normal file
79
src/store/dns-settings/reducer.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { createReducer } from 'typesafe-actions';
|
||||
import { combineReducers } from 'redux';
|
||||
import { DNSSettings } from './types';
|
||||
import actions, { ActionTypes } from './actions';
|
||||
import {ApiError, CreateResponse} from "../../services/api-client/types";
|
||||
|
||||
type StateType = Readonly<{
|
||||
data: DNSSettings | null;
|
||||
dnsSettings: DNSSettings | null;
|
||||
loading: boolean;
|
||||
failed: ApiError | null;
|
||||
saving: boolean;
|
||||
savedDNSSettings: CreateResponse<DNSSettings | null>;
|
||||
setupNewDNSSettingsVisible: boolean;
|
||||
setupNewDNSSettingsHA: boolean
|
||||
}>;
|
||||
|
||||
const initialState: StateType = {
|
||||
data: null,
|
||||
dnsSettings: null,
|
||||
loading: false,
|
||||
failed: null,
|
||||
saving: false,
|
||||
savedDNSSettings: <CreateResponse<DNSSettings | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
setupNewDNSSettingsVisible: false,
|
||||
setupNewDNSSettingsHA: false
|
||||
};
|
||||
|
||||
const data = createReducer<DNSSettings, ActionTypes>(initialState.data as DNSSettings)
|
||||
.handleAction(actions.getDNSSettings.success,(settings, action) => action.payload)
|
||||
.handleAction(actions.getDNSSettings.failure,(settings, _) => settings);
|
||||
|
||||
const dnsSettings = createReducer<DNSSettings, ActionTypes>(initialState.dnsSettings as DNSSettings)
|
||||
.handleAction(actions.setDNSSettings, (store, action) => action.payload);
|
||||
|
||||
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
|
||||
.handleAction(actions.getDNSSettings.request, () => true)
|
||||
.handleAction(actions.getDNSSettings.success, () => false)
|
||||
.handleAction(actions.getDNSSettings.failure, () => false);
|
||||
|
||||
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
|
||||
.handleAction(actions.getDNSSettings.request, () => null)
|
||||
.handleAction(actions.getDNSSettings.success, () => null)
|
||||
.handleAction(actions.getDNSSettings.failure, (store, action) => action.payload);
|
||||
|
||||
const saving = createReducer<boolean, ActionTypes>(initialState.saving)
|
||||
.handleAction(actions.getDNSSettings.request, () => true)
|
||||
.handleAction(actions.getDNSSettings.success, () => false)
|
||||
.handleAction(actions.getDNSSettings.failure, () => false);
|
||||
|
||||
const savedDNSSettings = createReducer<CreateResponse<DNSSettings | null>, ActionTypes>(initialState.savedDNSSettings)
|
||||
.handleAction(actions.saveDNSSettings.request, () => initialState.savedDNSSettings)
|
||||
.handleAction(actions.saveDNSSettings.success, (store, action) => action.payload)
|
||||
.handleAction(actions.saveDNSSettings.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setSavedDNSSettings, (store, action) => action.payload)
|
||||
.handleAction(actions.resetSavedDNSSettings, () => initialState.savedDNSSettings)
|
||||
|
||||
const setupNewDNSSettingsVisible = createReducer<boolean, ActionTypes>(initialState.setupNewDNSSettingsVisible)
|
||||
.handleAction(actions.setSetupNewDNSSettingsVisible, (store, action) => action.payload)
|
||||
|
||||
const setupNewDNSSettingsHA = createReducer<boolean, ActionTypes>(initialState.setupNewDNSSettingsHA)
|
||||
.handleAction(actions.setSetupNewDNSSettingsHA, (store, action) => action.payload)
|
||||
|
||||
export default combineReducers({
|
||||
data,
|
||||
dnsSettings,
|
||||
loading,
|
||||
failed,
|
||||
saving,
|
||||
savedDNSSettings,
|
||||
setupNewDNSSettingsVisible,
|
||||
setupNewDNSSettingsHA
|
||||
});
|
||||
95
src/store/dns-settings/sagas.ts
Normal file
95
src/store/dns-settings/sagas.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import {all, call, put, takeLatest} from 'redux-saga/effects';
|
||||
import {ApiError, ApiResponse, CreateResponse} from '../../services/api-client/types';
|
||||
import {DNSSettings} from './types'
|
||||
import service from './service';
|
||||
import actions from './actions';
|
||||
import serviceGroup from "../group/service";
|
||||
import {Group} from "../group/types";
|
||||
import {actions as groupActions} from "../group";
|
||||
|
||||
export function* getDNSSettings(action: ReturnType<typeof actions.getDNSSettings.request>): Generator {
|
||||
try {
|
||||
|
||||
const effect = yield call(service.getDNSSettings, action.payload);
|
||||
const response = effect as ApiResponse<DNSSettings>;
|
||||
|
||||
yield put(actions.getDNSSettings.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getDNSSettings.failure(err as ApiError));
|
||||
}
|
||||
}
|
||||
|
||||
export function* saveDNSSettings(action: ReturnType<typeof actions.saveDNSSettings.request>): Generator {
|
||||
try {
|
||||
yield put(actions.setSavedDNSSettings({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as CreateResponse<DNSSettings | null>))
|
||||
|
||||
const settingsToSave = action.payload.payload
|
||||
|
||||
let groupsToCreate = settingsToSave.groupsToCreate
|
||||
if (!groupsToCreate) {
|
||||
groupsToCreate = []
|
||||
}
|
||||
|
||||
// first, create groups that were newly added by user
|
||||
const responsesGroup = yield all(groupsToCreate.map(g => call(serviceGroup.createGroup, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: {name: g}
|
||||
})
|
||||
))
|
||||
|
||||
const resGroups = (responsesGroup as ApiResponse<Group>[]).filter(r => r.statusCode === 200).map(g => (g.body as Group)).map(g => g.id)
|
||||
const newGroups = [...settingsToSave.disabled_management_groups, ...resGroups]
|
||||
|
||||
const payloadToSave = {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: {
|
||||
disabled_management_groups: newGroups,
|
||||
} as DNSSettings
|
||||
}
|
||||
|
||||
let effect = yield call(service.editDNSSettings, payloadToSave);
|
||||
const response = effect as ApiResponse<DNSSettings>;
|
||||
|
||||
yield put(actions.saveDNSSettings.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as CreateResponse<DNSSettings | null>));
|
||||
|
||||
yield put(groupActions.getGroups.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
|
||||
yield put(actions.getDNSSettings.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
|
||||
|
||||
} catch (err) {
|
||||
yield put(groupActions.getGroups.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
yield put(actions.saveDNSSettings.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: true,
|
||||
error: err as ApiError,
|
||||
data: null
|
||||
} as CreateResponse<DNSSettings | null>));
|
||||
}
|
||||
}
|
||||
|
||||
export default function* sagas(): Generator {
|
||||
yield all([
|
||||
takeLatest(actions.getDNSSettings.request, getDNSSettings),
|
||||
takeLatest(actions.saveDNSSettings.request, saveDNSSettings),
|
||||
]);
|
||||
}
|
||||
|
||||
18
src/store/dns-settings/service.ts
Normal file
18
src/store/dns-settings/service.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
|
||||
import { apiClient } from '../../services/api-client';
|
||||
import { DNSSettings } from './types';
|
||||
|
||||
export default {
|
||||
async getDNSSettings(payload:RequestPayload<null>): Promise<ApiResponse<DNSSettings>> {
|
||||
return apiClient.get<DNSSettings>(
|
||||
`/api/dns/settings`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async editDNSSettings(payload:RequestPayload<DNSSettings>): Promise<ApiResponse<DNSSettings>> {
|
||||
return apiClient.put<DNSSettings>(
|
||||
`/api/dns/settings`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
};
|
||||
8
src/store/dns-settings/types.ts
Normal file
8
src/store/dns-settings/types.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface DNSSettings {
|
||||
disabled_management_groups: string[]
|
||||
}
|
||||
|
||||
export interface DNSSettingsToSave extends DNSSettings
|
||||
{
|
||||
groupsToCreate: string[]
|
||||
}
|
||||
14
src/store/event/actions.ts
Normal file
14
src/store/event/actions.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {ActionType, createAsyncAction} from 'typesafe-actions';
|
||||
import {Event} from './types';
|
||||
import {ApiError, RequestPayload} from '../../services/api-client/types';
|
||||
|
||||
const actions = {
|
||||
getEvents: createAsyncAction(
|
||||
'GET_EVENTS_REQUEST',
|
||||
'GET_EVENTS_SUCCESS',
|
||||
'GET_EVENTS_FAILURE',
|
||||
)<RequestPayload<null>, Event[], ApiError>(),
|
||||
};
|
||||
|
||||
export type ActionTypes = ActionType<typeof actions>;
|
||||
export default actions;
|
||||
7
src/store/event/index.ts
Normal file
7
src/store/event/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import actions, { ActionTypes as _actionTypes } from './actions';
|
||||
import reducer from './reducer';
|
||||
import sagas from './sagas';
|
||||
|
||||
export type ActionTypes = _actionTypes;
|
||||
|
||||
export { actions, reducer, sagas };
|
||||
38
src/store/event/reducer.ts
Normal file
38
src/store/event/reducer.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { createReducer } from 'typesafe-actions';
|
||||
import { combineReducers } from 'redux';
|
||||
import { Event } from './types';
|
||||
import actions, { ActionTypes } from './actions';
|
||||
import {ApiError, CreateResponse} from "../../services/api-client/types";
|
||||
|
||||
type StateType = Readonly<{
|
||||
data: Event[] | null;
|
||||
loading: boolean;
|
||||
failed: ApiError | null;
|
||||
}>;
|
||||
|
||||
const initialState: StateType = {
|
||||
data: [],
|
||||
loading: false,
|
||||
failed: null,
|
||||
};
|
||||
|
||||
const data = createReducer<Event[], ActionTypes>(initialState.data as Event[])
|
||||
.handleAction(actions.getEvents.success,(_, action) => action.payload)
|
||||
.handleAction(actions.getEvents.failure, () => []);
|
||||
|
||||
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
|
||||
.handleAction(actions.getEvents.request, () => true)
|
||||
.handleAction(actions.getEvents.success, () => false)
|
||||
.handleAction(actions.getEvents.failure, () => false);
|
||||
|
||||
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
|
||||
.handleAction(actions.getEvents.request, () => null)
|
||||
.handleAction(actions.getEvents.success, () => null)
|
||||
.handleAction(actions.getEvents.failure, (store, action) => action.payload);
|
||||
|
||||
|
||||
export default combineReducers({
|
||||
data,
|
||||
loading,
|
||||
failed,
|
||||
});
|
||||
23
src/store/event/sagas.ts
Normal file
23
src/store/event/sagas.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {all, call, put, takeLatest} from 'redux-saga/effects';
|
||||
import {ApiError, ApiResponse} from '../../services/api-client/types';
|
||||
import {Event} from './types'
|
||||
import service from './service';
|
||||
import actions from './actions';
|
||||
|
||||
export function* getEvents(action: ReturnType<typeof actions.getEvents.request>): Generator {
|
||||
try {
|
||||
const effect = yield call(service.getEvents, action.payload);
|
||||
const response = effect as ApiResponse<Event[]>;
|
||||
|
||||
yield put(actions.getEvents.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getEvents.failure(err as ApiError));
|
||||
}
|
||||
}
|
||||
|
||||
export default function* sagas(): Generator {
|
||||
yield all([
|
||||
takeLatest(actions.getEvents.request, getEvents),
|
||||
]);
|
||||
}
|
||||
|
||||
12
src/store/event/service.ts
Normal file
12
src/store/event/service.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
|
||||
import { apiClient } from '../../services/api-client';
|
||||
import {Event} from './types';
|
||||
|
||||
export default {
|
||||
async getEvents(payload:RequestPayload<null>): Promise<ApiResponse<Event[]>> {
|
||||
return apiClient.get<Event[]>(
|
||||
`/api/events`,
|
||||
payload
|
||||
);
|
||||
}
|
||||
};
|
||||
9
src/store/event/types.ts
Normal file
9
src/store/event/types.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface Event {
|
||||
id: string;
|
||||
timestamp: string
|
||||
activity: string
|
||||
activity_code: string
|
||||
initiator_id: string
|
||||
target_id: string
|
||||
meta: { [key: string]: string }
|
||||
}
|
||||
@@ -8,6 +8,11 @@ import { sagas as userSagas } from './user';
|
||||
import { sagas as ruleSagas } from './rule';
|
||||
import { sagas as groupSagas } from './group';
|
||||
import { sagas as routeSagas } from './route';
|
||||
import { sagas as nameserverGroupSagas } from './nameservers';
|
||||
import { sagas as eventSagas } from './event';
|
||||
import { sagas as dnsSettingsSagas } from './dns-settings';
|
||||
import { sagas as accountSagas } from './account';
|
||||
import { sagas as personalAccessTokenSagas } from './personal-access-token';
|
||||
|
||||
import rootReducer from './root-reducer';
|
||||
import { apiClient } from '../services/api-client';
|
||||
@@ -25,5 +30,10 @@ sagaMiddleware.run(userSagas);
|
||||
sagaMiddleware.run(ruleSagas);
|
||||
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 };
|
||||
35
src/store/nameservers/actions.ts
Normal file
35
src/store/nameservers/actions.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ActionType, createAction, createAsyncAction } from 'typesafe-actions';
|
||||
import {NameServerGroup, NameServerGroupToSave} from './types';
|
||||
import {ApiError, CreateResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
|
||||
|
||||
const actions = {
|
||||
getNameServerGroups: createAsyncAction(
|
||||
'GET_NameServerGroup_REQUEST',
|
||||
'GET_NameServerGroup_SUCCESS',
|
||||
'GET_NameServerGroup_FAILURE',
|
||||
)<RequestPayload<null>, NameServerGroup[], ApiError>(),
|
||||
|
||||
saveNameServerGroup: createAsyncAction(
|
||||
'SAVE_NameServerGroup_REQUEST',
|
||||
'SAVE_NameServerGroup_SUCCESS',
|
||||
'SAVE_NameServerGroup_FAILURE',
|
||||
)<RequestPayload<NameServerGroupToSave>, CreateResponse<NameServerGroup | null>, CreateResponse<NameServerGroup | null>>(),
|
||||
setSavedNameServerGroup: createAction('SET_CREATE_NameServerGroup')<CreateResponse<NameServerGroup | null>>(),
|
||||
resetSavedNameServerGroup: createAction('RESET_CREATE_NameServerGroup')<null>(),
|
||||
|
||||
deleteNameServerGroup: createAsyncAction(
|
||||
'DELETE_NameServerGroup_REQUEST',
|
||||
'DELETE_NameServerGroup_SUCCESS',
|
||||
'DELETE_NameServerGroup_FAILURE'
|
||||
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
|
||||
setDeletedNameServerGroup: createAction('SET_DELETED_NameServerGroup')<DeleteResponse<string | null>>(),
|
||||
resetDeletedNameServerGroup: createAction('RESET_DELETED_NameServerGroup')<null>(),
|
||||
removeNameServerGroup: createAction('REMOVE_NameServerGroup')<string>(),
|
||||
|
||||
setNameServerGroup: createAction('SET_NameServerGroup')<NameServerGroup>(),
|
||||
setSetupNewNameServerGroupVisible: createAction('SET_SETUP_NEW_NameServerGroup_VISIBLE')<boolean>(),
|
||||
setSetupNewNameServerGroupHA: createAction('SET_SETUP_NEW_NameServerGroup_HA')<boolean>()
|
||||
};
|
||||
|
||||
export type ActionTypes = ActionType<typeof actions>;
|
||||
export default actions;
|
||||
7
src/store/nameservers/index.ts
Normal file
7
src/store/nameservers/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import actions, { ActionTypes as _actionTypes } from './actions';
|
||||
import reducer from './reducer';
|
||||
import sagas from './sagas';
|
||||
|
||||
export type ActionTypes = _actionTypes;
|
||||
|
||||
export { actions, reducer, sagas };
|
||||
95
src/store/nameservers/reducer.ts
Normal file
95
src/store/nameservers/reducer.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { createReducer } from 'typesafe-actions';
|
||||
import { combineReducers } from 'redux';
|
||||
import { NameServerGroup } from './types';
|
||||
import actions, { ActionTypes } from './actions';
|
||||
import {ApiError, DeleteResponse, CreateResponse} from "../../services/api-client/types";
|
||||
|
||||
type StateType = Readonly<{
|
||||
data: NameServerGroup[] | null;
|
||||
nameserverGroup: NameServerGroup | null;
|
||||
loading: boolean;
|
||||
failed: ApiError | null;
|
||||
saving: boolean;
|
||||
deleteNameServerGroup: DeleteResponse<string | null>;
|
||||
savedNameServerGroup: CreateResponse<NameServerGroup | null>;
|
||||
setupNewNameServerGroupVisible: boolean;
|
||||
setupNewNameServerGroupHA: boolean
|
||||
}>;
|
||||
|
||||
const initialState: StateType = {
|
||||
data: [],
|
||||
nameserverGroup: null,
|
||||
loading: false,
|
||||
failed: null,
|
||||
saving: false,
|
||||
deleteNameServerGroup: <DeleteResponse<string | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
savedNameServerGroup: <CreateResponse<NameServerGroup | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
setupNewNameServerGroupVisible: false,
|
||||
setupNewNameServerGroupHA: false
|
||||
};
|
||||
|
||||
const data = createReducer<NameServerGroup[], ActionTypes>(initialState.data as NameServerGroup[])
|
||||
.handleAction(actions.getNameServerGroups.success,(_, action) => action.payload)
|
||||
.handleAction(actions.getNameServerGroups.failure, () => []);
|
||||
|
||||
const nameserverGroup = createReducer<NameServerGroup, ActionTypes>(initialState.nameserverGroup as NameServerGroup)
|
||||
.handleAction(actions.setNameServerGroup, (store, action) => action.payload);
|
||||
|
||||
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
|
||||
.handleAction(actions.getNameServerGroups.request, () => true)
|
||||
.handleAction(actions.getNameServerGroups.success, () => false)
|
||||
.handleAction(actions.getNameServerGroups.failure, () => false);
|
||||
|
||||
const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
|
||||
.handleAction(actions.getNameServerGroups.request, () => null)
|
||||
.handleAction(actions.getNameServerGroups.success, () => null)
|
||||
.handleAction(actions.getNameServerGroups.failure, (store, action) => action.payload);
|
||||
|
||||
const saving = createReducer<boolean, ActionTypes>(initialState.saving)
|
||||
.handleAction(actions.getNameServerGroups.request, () => true)
|
||||
.handleAction(actions.getNameServerGroups.success, () => false)
|
||||
.handleAction(actions.getNameServerGroups.failure, () => false);
|
||||
|
||||
const deletedNameServerGroup = createReducer<DeleteResponse<string | null>, ActionTypes>(initialState.deleteNameServerGroup)
|
||||
.handleAction(actions.deleteNameServerGroup.request, () => initialState.deleteNameServerGroup)
|
||||
.handleAction(actions.deleteNameServerGroup.success, (store, action) => action.payload)
|
||||
.handleAction(actions.deleteNameServerGroup.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setDeletedNameServerGroup, (store, action) => action.payload)
|
||||
.handleAction(actions.resetDeletedNameServerGroup, () => initialState.deleteNameServerGroup)
|
||||
|
||||
const savedNameServerGroup = createReducer<CreateResponse<NameServerGroup | null>, ActionTypes>(initialState.savedNameServerGroup)
|
||||
.handleAction(actions.saveNameServerGroup.request, () => initialState.savedNameServerGroup)
|
||||
.handleAction(actions.saveNameServerGroup.success, (store, action) => action.payload)
|
||||
.handleAction(actions.saveNameServerGroup.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setSavedNameServerGroup, (store, action) => action.payload)
|
||||
.handleAction(actions.resetSavedNameServerGroup, () => initialState.savedNameServerGroup)
|
||||
|
||||
const setupNewNameServerGroupVisible = createReducer<boolean, ActionTypes>(initialState.setupNewNameServerGroupVisible)
|
||||
.handleAction(actions.setSetupNewNameServerGroupVisible, (store, action) => action.payload)
|
||||
|
||||
const setupNewNameServerGroupHA = createReducer<boolean, ActionTypes>(initialState.setupNewNameServerGroupHA)
|
||||
.handleAction(actions.setSetupNewNameServerGroupHA, (store, action) => action.payload)
|
||||
|
||||
export default combineReducers({
|
||||
data,
|
||||
nameserverGroup,
|
||||
loading,
|
||||
failed,
|
||||
saving,
|
||||
deletedNameServerGroup,
|
||||
savedNameServerGroup,
|
||||
setupNewNameServerGroupVisible,
|
||||
setupNewNameServerGroupHA
|
||||
});
|
||||
160
src/store/nameservers/sagas.ts
Normal file
160
src/store/nameservers/sagas.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import {all, call, put, select, takeLatest} from 'redux-saga/effects';
|
||||
import {ApiError, ApiResponse, CreateResponse, DeleteResponse} from '../../services/api-client/types';
|
||||
import {NameServerGroup} from './types'
|
||||
import service from './service';
|
||||
import actions from './actions';
|
||||
import serviceGroup from "../group/service";
|
||||
import {Group} from "../group/types";
|
||||
import {actions as groupActions} from "../group";
|
||||
|
||||
export function* getNameServerGroups(action: ReturnType<typeof actions.getNameServerGroups.request>): Generator {
|
||||
try {
|
||||
|
||||
yield put(actions.setDeletedNameServerGroup({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>))
|
||||
|
||||
const effect = yield call(service.getNameServerGroups, action.payload);
|
||||
const response = effect as ApiResponse<NameServerGroup[]>;
|
||||
|
||||
yield put(actions.getNameServerGroups.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getNameServerGroups.failure(err as ApiError));
|
||||
}
|
||||
}
|
||||
|
||||
export function* setCreatedNameServerGroup(action: ReturnType<typeof actions.setSavedNameServerGroup>): Generator {
|
||||
yield put(actions.setSavedNameServerGroup(action.payload))
|
||||
}
|
||||
|
||||
export function* saveNameServerGroup(action: ReturnType<typeof actions.saveNameServerGroup.request>): Generator {
|
||||
try {
|
||||
yield put(actions.setSavedNameServerGroup({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as CreateResponse<NameServerGroup | null>))
|
||||
|
||||
const nameserverGroupToSave = action.payload.payload
|
||||
|
||||
let groupsToCreate = nameserverGroupToSave.groupsToCreate
|
||||
if (!groupsToCreate) {
|
||||
groupsToCreate = []
|
||||
}
|
||||
|
||||
// first, create groups that were newly added by user
|
||||
const responsesGroup = yield all(groupsToCreate.map(g => call(serviceGroup.createGroup, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: {name: g}
|
||||
})
|
||||
))
|
||||
|
||||
const resGroups = (responsesGroup as ApiResponse<Group>[]).filter(r => r.statusCode === 200).map(g => (g.body as Group)).map(g => g.id)
|
||||
const newGroups = [...nameserverGroupToSave.groups, ...resGroups]
|
||||
|
||||
const payloadToSave = {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: {
|
||||
id: nameserverGroupToSave.id,
|
||||
name: nameserverGroupToSave.name,
|
||||
description: nameserverGroupToSave.description,
|
||||
primary: nameserverGroupToSave.primary,
|
||||
domains: nameserverGroupToSave.domains,
|
||||
nameservers: nameserverGroupToSave.nameservers,
|
||||
groups: newGroups,
|
||||
enabled: nameserverGroupToSave.enabled,
|
||||
} as NameServerGroup
|
||||
}
|
||||
|
||||
let effect
|
||||
if (!nameserverGroupToSave.id) {
|
||||
effect = yield call(service.createNameServerGroup, payloadToSave);
|
||||
} else {
|
||||
payloadToSave.payload.id = nameserverGroupToSave.id
|
||||
effect = yield call(service.editNameServerGroup, payloadToSave);
|
||||
}
|
||||
|
||||
const response = effect as ApiResponse<NameServerGroup>;
|
||||
|
||||
yield put(actions.saveNameServerGroup.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as CreateResponse<NameServerGroup | null>));
|
||||
|
||||
yield put(groupActions.getGroups.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
|
||||
yield put(actions.getNameServerGroups.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
|
||||
|
||||
} catch (err) {
|
||||
yield put(groupActions.getGroups.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
yield put(actions.saveNameServerGroup.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: true,
|
||||
error: err as ApiError,
|
||||
data: null
|
||||
} as CreateResponse<NameServerGroup | null>));
|
||||
}
|
||||
}
|
||||
|
||||
export function* setDeleteNameServerGroup(action: ReturnType<typeof actions.setDeletedNameServerGroup>): Generator {
|
||||
yield put(actions.setDeletedNameServerGroup(action.payload))
|
||||
}
|
||||
|
||||
export function* deleteNameServerGroup(action: ReturnType<typeof actions.deleteNameServerGroup.request>): Generator {
|
||||
try {
|
||||
yield call(actions.setDeletedNameServerGroup,{
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>)
|
||||
|
||||
const effect = yield call(service.deletedNameServerGroup, action.payload);
|
||||
const response = effect as ApiResponse<any>;
|
||||
|
||||
yield put(actions.deleteNameServerGroup.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as DeleteResponse<string | null>));
|
||||
|
||||
const nameserverGroup = (yield select(state => state.nameserverGroup.data)) as NameServerGroup[]
|
||||
yield put(actions.getNameServerGroups.success(nameserverGroup.filter((p:NameServerGroup) => p.id !== action.payload.payload)))
|
||||
} catch (err) {
|
||||
yield put(actions.deleteNameServerGroup.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: err as ApiError,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>));
|
||||
}
|
||||
}
|
||||
|
||||
export default function* sagas(): Generator {
|
||||
yield all([
|
||||
takeLatest(actions.getNameServerGroups.request, getNameServerGroups),
|
||||
takeLatest(actions.saveNameServerGroup.request, saveNameServerGroup),
|
||||
takeLatest(actions.deleteNameServerGroup.request, deleteNameServerGroup)
|
||||
]);
|
||||
}
|
||||
|
||||
32
src/store/nameservers/service.ts
Normal file
32
src/store/nameservers/service.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
|
||||
import { apiClient } from '../../services/api-client';
|
||||
import { NameServerGroup } from './types';
|
||||
|
||||
export default {
|
||||
async getNameServerGroups(payload:RequestPayload<null>): Promise<ApiResponse<NameServerGroup[]>> {
|
||||
return apiClient.get<NameServerGroup[]>(
|
||||
`/api/dns/nameservers`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async deletedNameServerGroup(payload:RequestPayload<string>): Promise<ApiResponse<any>> {
|
||||
return apiClient.delete<any>(
|
||||
`/api/dns/nameservers/` + payload.payload,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async createNameServerGroup(payload:RequestPayload<NameServerGroup>): Promise<ApiResponse<NameServerGroup>> {
|
||||
return apiClient.post<NameServerGroup>(
|
||||
`/api/dns/nameservers`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async editNameServerGroup(payload:RequestPayload<NameServerGroup>): Promise<ApiResponse<NameServerGroup>> {
|
||||
const id = payload.payload.id
|
||||
delete payload.payload.id
|
||||
return apiClient.put<NameServerGroup>(
|
||||
`/api/dns/nameservers/${id}`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
};
|
||||
21
src/store/nameservers/types.ts
Normal file
21
src/store/nameservers/types.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export interface NameServerGroup {
|
||||
id?: string
|
||||
name: string
|
||||
description: string
|
||||
primary: boolean
|
||||
domains: string[]
|
||||
nameservers: NameServer[]
|
||||
groups: string[]
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface NameServer {
|
||||
ip: string
|
||||
ns_type: string
|
||||
port: number
|
||||
}
|
||||
|
||||
export interface NameServerGroupToSave extends NameServerGroup
|
||||
{
|
||||
groupsToCreate: string[]
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export function* deletePeer(action: ReturnType<typeof actions.deletedPeer.reques
|
||||
} as DeleteResponse<string | null>));
|
||||
|
||||
const peers = (yield select(state => state.peer.data)) as Peer[]
|
||||
yield put(actions.getPeers.success(peers.filter((p:Peer) => p.ip !== action.payload.payload)))
|
||||
yield put(actions.getPeers.success(peers.filter((p:Peer) => p.id !== action.payload.payload)))
|
||||
} catch (err) {
|
||||
yield put(actions.deletedPeer.failure({
|
||||
loading: false,
|
||||
|
||||
@@ -10,6 +10,18 @@ export interface Peer {
|
||||
version: string,
|
||||
groups?: Group[]
|
||||
ssh_enabled: boolean,
|
||||
hostname: string,
|
||||
user_id?: string,
|
||||
ui_version?: string,
|
||||
dns_label: string,
|
||||
last_login: string,
|
||||
login_expired: boolean,
|
||||
login_expiration_enabled: boolean
|
||||
}
|
||||
|
||||
export interface FormPeer extends Peer {
|
||||
groupsNames: string[],
|
||||
userEmail?: string
|
||||
}
|
||||
|
||||
export interface PeerToSave extends Peer {
|
||||
@@ -30,3 +42,13 @@ export interface PeerNameToIP {
|
||||
export interface PeerIPToName {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface PeerIPToID {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface PeerDataTable extends Peer {
|
||||
key: string;
|
||||
groups: Group[];
|
||||
groupsCount: number;
|
||||
}
|
||||
|
||||
41
src/store/personal-access-token/actions.ts
Normal file
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
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
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
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
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
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,
|
||||
}
|
||||
@@ -1,9 +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 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,
|
||||
@@ -11,5 +16,10 @@ export default {
|
||||
user: UserActions,
|
||||
group: GroupActions,
|
||||
rule: RuleActions,
|
||||
route: RouteActions
|
||||
route: RouteActions,
|
||||
nameserverGroup: NameServerGroupActions,
|
||||
event: EventActions,
|
||||
dnsSettings: DNSSettingsActions,
|
||||
account: AccountActions,
|
||||
personalAccessToken: PersonalAccessTokenActions
|
||||
};
|
||||
|
||||
@@ -6,6 +6,11 @@ import { reducer as user } from './user';
|
||||
import { reducer as group } from './group';
|
||||
import { reducer as rule } from './rule';
|
||||
import { reducer as route } from './route';
|
||||
import { reducer as nameserverGroup } from './nameservers';
|
||||
import { reducer as event } from './event';
|
||||
import { reducer as dnsSettings } from './dns-settings';
|
||||
import { reducer as account } from './account';
|
||||
import { reducer as personalAccessToken } from './personal-access-token';
|
||||
|
||||
export default combineReducers({
|
||||
peer,
|
||||
@@ -13,5 +18,10 @@ export default combineReducers({
|
||||
user,
|
||||
group,
|
||||
rule,
|
||||
route
|
||||
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>(),
|
||||
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
|
||||
export interface Route {
|
||||
id?: string
|
||||
description: string
|
||||
@@ -8,4 +9,10 @@ export interface Route {
|
||||
network_type?: string
|
||||
metric?: number
|
||||
masquerade: boolean
|
||||
groups: string[]
|
||||
}
|
||||
|
||||
export interface RouteToSave extends Route
|
||||
{
|
||||
groupsToCreate: string[]
|
||||
}
|
||||
@@ -61,7 +61,9 @@ export function* saveSetupKey(action: ReturnType<typeof actions.saveSetupKey.req
|
||||
payload: {
|
||||
name: keyToSave.name,
|
||||
auto_groups: newGroups,
|
||||
type: keyToSave.type
|
||||
type: keyToSave.type,
|
||||
expires_in: keyToSave.expires_in,
|
||||
usage_limit: keyToSave.usage_limit
|
||||
} as SetupKeyToSave
|
||||
});
|
||||
} else {
|
||||
@@ -72,6 +74,7 @@ export function* saveSetupKey(action: ReturnType<typeof actions.saveSetupKey.req
|
||||
name: keyToSave.name,
|
||||
revoked: keyToSave.revoked,
|
||||
auto_groups: newGroups,
|
||||
usage_limit: keyToSave.usage_limit
|
||||
} as SetupKeyToSave
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {Group} from "../group/types";
|
||||
import {ExpiresInValue} from "../../views/ExpiresInInput";
|
||||
import moment from "moment";
|
||||
|
||||
export interface SetupKey {
|
||||
expires: string;
|
||||
@@ -12,6 +13,15 @@ export interface SetupKey {
|
||||
used_times: number;
|
||||
valid: boolean;
|
||||
auto_groups: string[]
|
||||
expires_in: number;
|
||||
usage_limit: number;
|
||||
}
|
||||
|
||||
export interface FormSetupKey extends SetupKey {
|
||||
autoGroupNames: string[]
|
||||
expiresInFormatted: ExpiresInValue
|
||||
exp: moment.Moment
|
||||
last: moment.Moment
|
||||
}
|
||||
|
||||
export interface SetupKeyToSave extends SetupKey
|
||||
|
||||
@@ -1,13 +1,54 @@
|
||||
import { ActionType, createAsyncAction } from 'typesafe-actions';
|
||||
import { User } from './types';
|
||||
import { ApiError, RequestPayload } from '../../services/api-client/types';
|
||||
import {ActionType, createAction, createAsyncAction} from 'typesafe-actions';
|
||||
import {User, UserToSave} from './types';
|
||||
import {ApiError, CreateResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
|
||||
|
||||
const actions = {
|
||||
getUsers: createAsyncAction(
|
||||
'GET_USERS_REQUEST',
|
||||
'GET_USERS_SUCCESS',
|
||||
'GET_USERS_FAILURE',
|
||||
)<RequestPayload<null>, User[], ApiError>()
|
||||
)<RequestPayload<null>, User[], ApiError>(),
|
||||
|
||||
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',
|
||||
'SAVE_USER_SUCCESS',
|
||||
'SAVE_USER_FAILURE',
|
||||
)<RequestPayload<UserToSave>, CreateResponse<User | null>, CreateResponse<User | null>>(),
|
||||
setSavedUser: createAction('SET_SAVED_USER')<CreateResponse<User | null>>(),
|
||||
resetSavedUser: createAction('RESET_SAVED_USER')<null>(),
|
||||
};
|
||||
|
||||
export type ActionTypes = ActionType<typeof actions>;
|
||||
|
||||
@@ -2,24 +2,65 @@ import { createReducer } from 'typesafe-actions';
|
||||
import { combineReducers } from 'redux';
|
||||
import { User } from './types';
|
||||
import actions, { ActionTypes } from './actions';
|
||||
import { ApiError } from "../../services/api-client/types";
|
||||
import {ApiError, CreateResponse, 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,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
};
|
||||
|
||||
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)
|
||||
@@ -30,9 +71,50 @@ const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
|
||||
.handleAction(actions.getUsers.success, () => null)
|
||||
.handleAction(actions.getUsers.failure, (store, action) => action.payload);
|
||||
|
||||
const user = createReducer<User, ActionTypes>(initialState.user as User)
|
||||
.handleAction(actions.setUser, (store, action) => action.payload);
|
||||
|
||||
const 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)
|
||||
.handleAction(actions.saveUser.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setSavedUser, (store, action) => action.payload)
|
||||
.handleAction(actions.resetSavedUser, () => initialState.savedUser)
|
||||
|
||||
export default combineReducers({
|
||||
data,
|
||||
serviceUsers,
|
||||
regularUsers,
|
||||
loading,
|
||||
failed
|
||||
failed,
|
||||
user,
|
||||
savedUser,
|
||||
deletedUser,
|
||||
updateUserDrawerVisible,
|
||||
inviteUserPopupVisible,
|
||||
editUserPopupVisible,
|
||||
addServiceUserPopupVisible,
|
||||
userTabOpen
|
||||
});
|
||||
|
||||
@@ -1,23 +1,160 @@
|
||||
import {all, call, put, takeLatest} from 'redux-saga/effects';
|
||||
import {ApiError, ApiResponse} from '../../services/api-client/types';
|
||||
import { User } from './types'
|
||||
import {all, call, put, select, takeLatest} from 'redux-saga/effects';
|
||||
import {ApiError, ApiResponse, CreateResponse, DeleteResponse} from '../../services/api-client/types';
|
||||
import {User, UserToSave} from './types'
|
||||
import service from './service';
|
||||
import actions from './actions';
|
||||
import serviceGroup from "../group/service";
|
||||
import {Group} from "../group/types";
|
||||
import {actions as groupActions} from "../group";
|
||||
import {PersonalAccessToken} from "../personal-access-token/types";
|
||||
|
||||
export function* getPeers(action: ReturnType<typeof actions.getUsers.request>): Generator {
|
||||
try {
|
||||
const effect = yield call(service.getUsers, action.payload);
|
||||
const response = effect as ApiResponse<User[]>;
|
||||
export function* getUsers(action: ReturnType<typeof actions.getUsers.request>): Generator {
|
||||
try {
|
||||
const effect = yield call(service.getUsers, action.payload);
|
||||
const response = effect as ApiResponse<User[]>;
|
||||
|
||||
yield put(actions.getUsers.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getUsers.failure(err as ApiError));
|
||||
}
|
||||
yield put(actions.getUsers.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getUsers.failure(err as ApiError));
|
||||
}
|
||||
}
|
||||
|
||||
export function* getServiceUsers(action: ReturnType<typeof actions.getServiceUsers.request>): Generator {
|
||||
try {
|
||||
action.payload.queryParams = {service_user: true}
|
||||
const effect = yield call(service.getUsers, action.payload);
|
||||
const response = effect as ApiResponse<User[]>;
|
||||
|
||||
yield put(actions.getServiceUsers.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getServiceUsers.failure(err as ApiError));
|
||||
}
|
||||
}
|
||||
|
||||
export function* getRegularUsers(action: ReturnType<typeof actions.getRegularUsers.request>): Generator {
|
||||
try {
|
||||
action.payload.queryParams = {service_user: false}
|
||||
const effect = yield call(service.getUsers, action.payload);
|
||||
const response = effect as ApiResponse<User[]>;
|
||||
|
||||
yield put(actions.getRegularUsers.success(response.body));
|
||||
} catch (err) {
|
||||
yield put(actions.getRegularUsers.failure(err as ApiError));
|
||||
}
|
||||
}
|
||||
|
||||
export function* saveUser(action: ReturnType<typeof actions.saveUser.request>): Generator {
|
||||
try {
|
||||
yield put(actions.setSavedUser({
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as CreateResponse<User | null>))
|
||||
|
||||
const userToSave = action.payload.payload
|
||||
|
||||
let groupsToCreate = userToSave.groupsToCreate
|
||||
if (!groupsToCreate) {
|
||||
groupsToCreate = []
|
||||
}
|
||||
|
||||
// first, create groups that were newly added by user
|
||||
const responsesGroup = yield all(groupsToCreate.map(g => call(serviceGroup.createGroup, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: {name: g}
|
||||
})
|
||||
))
|
||||
|
||||
const resGroups = (responsesGroup as ApiResponse<Group>[]).filter(r => r.statusCode === 200).map(g => (g.body as Group)).map(g => g.id)
|
||||
const newGroups = [...userToSave.auto_groups, ...resGroups]
|
||||
let payload = {
|
||||
name: userToSave.name,
|
||||
email: userToSave.email,
|
||||
role: userToSave.role,
|
||||
auto_groups: newGroups,
|
||||
is_service_user: userToSave.is_service_user
|
||||
} as UserToSave
|
||||
|
||||
let effect
|
||||
if (!userToSave.id) {
|
||||
effect = yield call(service.createUser, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: payload
|
||||
});
|
||||
} else {
|
||||
payload.id = userToSave.id
|
||||
effect = yield call(service.editUser, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: payload
|
||||
});
|
||||
}
|
||||
const response = effect as ApiResponse<User>;
|
||||
|
||||
yield put(actions.saveUser.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as CreateResponse<User | null>));
|
||||
} catch (err) {
|
||||
yield put(actions.saveUser.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: err as ApiError,
|
||||
data: null
|
||||
} as CreateResponse<User | null>));
|
||||
}
|
||||
}
|
||||
|
||||
export function* deleteUser(action: ReturnType<typeof actions.deleteUser.request>): Generator {
|
||||
try {
|
||||
yield call(actions.setDeletedUser,{
|
||||
loading: true,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>)
|
||||
|
||||
const effect = yield call(service.deleteUser, action.payload);
|
||||
const response = effect as ApiResponse<any>;
|
||||
|
||||
yield put(actions.deleteUser.success({
|
||||
loading: false,
|
||||
success: true,
|
||||
failure: false,
|
||||
error: null,
|
||||
data: response.body
|
||||
} as DeleteResponse<string | null>));
|
||||
|
||||
const users = (yield select(state => state.users.data)) as User[]
|
||||
const regularUsers = (yield select(state => state.users.regularUsers)) as User[]
|
||||
const serviceUsers = (yield select(state => state.users.serviceUsers)) as User[]
|
||||
yield put(actions.getUsers.success(users.filter((p:User) => p.id !== action.payload.payload)))
|
||||
yield put(actions.getRegularUsers.success(regularUsers.filter((p:User) => p.id !== action.payload.payload)))
|
||||
yield put(actions.getServiceUsers.success(serviceUsers.filter((p:User) => p.id !== action.payload.payload)))
|
||||
} catch (err) {
|
||||
yield put(actions.deleteUser.failure({
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: err as ApiError,
|
||||
data: null
|
||||
} as DeleteResponse<string | null>));
|
||||
}
|
||||
}
|
||||
|
||||
export default function* sagas(): Generator {
|
||||
yield all([
|
||||
takeLatest(actions.getUsers.request, getPeers)
|
||||
]);
|
||||
yield all([
|
||||
takeLatest(actions.getUsers.request, getUsers),
|
||||
takeLatest(actions.getServiceUsers.request, getServiceUsers),
|
||||
takeLatest(actions.getRegularUsers.request, getRegularUsers),
|
||||
takeLatest(actions.saveUser.request, saveUser),
|
||||
takeLatest(actions.deleteUser.request, deleteUser)
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {ApiResponse, RequestPayload} from '../../services/api-client/types';
|
||||
import { apiClient } from '../../services/api-client';
|
||||
import { User } from './types';
|
||||
import {User, UserToSave} from './types';
|
||||
import {SpecificPAT} from "../personal-access-token/types";
|
||||
|
||||
export default {
|
||||
async getUsers(payload:RequestPayload<null>): Promise<ApiResponse<User[]>> {
|
||||
@@ -8,5 +9,27 @@ export default {
|
||||
`/api/users`,
|
||||
payload
|
||||
);
|
||||
}
|
||||
},
|
||||
async editUser(payload:RequestPayload<UserToSave>): Promise<ApiResponse<User>> {
|
||||
const id = payload.payload.id
|
||||
// @ts-ignore
|
||||
delete payload.payload.id
|
||||
return apiClient.put<User>(
|
||||
`/api/users/${id}`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async createUser(payload:RequestPayload<UserToSave>): Promise<ApiResponse<User>> {
|
||||
// @ts-ignore
|
||||
return apiClient.post<User>(
|
||||
`/api/users`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
async deleteUser(payload:RequestPayload<string>): Promise<ApiResponse<any>> {
|
||||
return apiClient.delete<any>(
|
||||
`/api/users/` + payload.payload,
|
||||
payload
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,4 +3,16 @@ export interface User {
|
||||
email?: string;
|
||||
name: string;
|
||||
role: string;
|
||||
}
|
||||
status: string;
|
||||
auto_groups: string[];
|
||||
is_current?: boolean;
|
||||
is_service_user?: boolean;
|
||||
}
|
||||
|
||||
export interface FormUser extends User {
|
||||
autoGroupsNames: string[]
|
||||
}
|
||||
|
||||
export interface UserToSave extends User {
|
||||
groupsToCreate: string[]
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
|
||||
|
||||
export const formatOS = (os) => {
|
||||
if (os.startsWith("windows 10")) {
|
||||
return "Windows 10";
|
||||
@@ -6,14 +8,39 @@ export const formatOS = (os) => {
|
||||
if (os.startsWith("Darwin")) {
|
||||
return os.replace("Darwin", "MacOS");
|
||||
}
|
||||
|
||||
// capitalize first letter
|
||||
os = os.charAt(0).toUpperCase() + os.slice(1);
|
||||
return os;
|
||||
};
|
||||
|
||||
export const formatDate = date => {
|
||||
if (new Date(date).getTime() > new Date("2099-12-31").getTime()) {
|
||||
return new Date(date).toLocaleDateString("en-GB", { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' });
|
||||
}
|
||||
return new Date(date).toLocaleDateString("en-GB", { weekday: 'short', year: '2-digit', month: 'short', day: 'numeric' });
|
||||
}
|
||||
|
||||
export const capitalize = text => {
|
||||
if (!text) {
|
||||
return text
|
||||
}
|
||||
return text.charAt(0).toUpperCase() + text.slice(1)
|
||||
}
|
||||
|
||||
export const checkExpiresIn = (_, value) => {
|
||||
if (value.number > 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error("Expiration must be greater than zero"));
|
||||
};
|
||||
|
||||
export const formatDateTime = date => {
|
||||
if (new Date(date).getTime() > new Date("2099-12-31").getTime()) {
|
||||
return new Date(date).toLocaleDateString("en-GB", { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' });
|
||||
}
|
||||
return new Date(date).toLocaleDateString("en-GB", { weekday: 'short', year: '2-digit', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' });
|
||||
}
|
||||
|
||||
export const classNames = (...classes) => {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
}
|
||||
@@ -50,6 +77,15 @@ function getFormattedDate(date, preformattedDate = false, hideYear = false) {
|
||||
return `${ day }. ${ month } ${ year }`;
|
||||
}
|
||||
|
||||
export const fullDate = (dateParam) => {
|
||||
if (!dateParam) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const date = typeof dateParam === 'object' ? dateParam : new Date(dateParam);
|
||||
return getFormattedDate(date);
|
||||
}
|
||||
|
||||
export const timeAgo = (dateParam) => {
|
||||
if (!dateParam) {
|
||||
return null;
|
||||
@@ -66,7 +102,9 @@ export const timeAgo = (dateParam) => {
|
||||
const isThisYear = today.getFullYear() === date.getFullYear();
|
||||
|
||||
|
||||
if (seconds < 5) {
|
||||
if (seconds < -1) {
|
||||
return getFormattedDate(date, false, true);
|
||||
} else if (seconds < 5) {
|
||||
return 'just now';
|
||||
} else if (seconds < 60) {
|
||||
return `${ seconds } seconds ago`;
|
||||
@@ -75,9 +113,9 @@ export const timeAgo = (dateParam) => {
|
||||
} else if (minutes < 60) {
|
||||
return `${ minutes } minutes ago`;
|
||||
} else if (isToday) {
|
||||
return getFormattedDate(date, 'Today'); // Today at 10:20
|
||||
return getFormattedDate(date, 'today'); // Today at 10:20
|
||||
} else if (isYesterday) {
|
||||
return getFormattedDate(date, 'Yesterday'); // Yesterday at 10:20
|
||||
return getFormattedDate(date, 'yesterday'); // Yesterday at 10:20
|
||||
} else if (isThisYear) {
|
||||
return getFormattedDate(date, false, true); // 10. January at 10:20
|
||||
}
|
||||
@@ -87,4 +125,12 @@ export const timeAgo = (dateParam) => {
|
||||
|
||||
export const copyToClipboard = (copyText) => {
|
||||
navigator.clipboard.writeText(copyText);
|
||||
}
|
||||
|
||||
export const isNetBirdHosted = () => {
|
||||
return window.location.hostname.endsWith(".netbird.io") || window.location.hostname.endsWith(".wiretrustee.com")
|
||||
}
|
||||
|
||||
export const isLocalDev = () => {
|
||||
return window.location.hostname.includes("localhost")
|
||||
}
|
||||
141
src/utils/groups.tsx
Normal file
141
src/utils/groups.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import {CustomTagProps} from "rc-select/lib/BaseSelect";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {Col, Divider, Row, Tag} from "antd";
|
||||
import {useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
|
||||
export const useGetGroupTagHelpers = () => {
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
|
||||
const [tagGroups, setTagGroups] = useState([] as string[])
|
||||
const [groupTagFilterAll, setGroupTagFilterAll] = useState(false)
|
||||
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[])
|
||||
|
||||
const tagRender = (props: CustomTagProps) => {
|
||||
const {value, closable, onClose} = props;
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<Tag
|
||||
color="blue"
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{value}</strong>
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
const handleChangeTags = (value: string[]) => {
|
||||
let validatedValues: string[] = []
|
||||
value.forEach(function (v) {
|
||||
if (v.trim().length) {
|
||||
validatedValues.push(v)
|
||||
}
|
||||
})
|
||||
setSelectedTagGroups(validatedValues)
|
||||
};
|
||||
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{margin: '8px 0'}}/>
|
||||
<Row style={{padding: '0 8px 4px'}}>
|
||||
<Col flex="auto">
|
||||
<span style={{color: "#9CA3AF"}}>Add new group by pressing "Enter"</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
|
||||
fill="#9CA3AF"/>
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
|
||||
const optionRender = (label: string) => {
|
||||
let peersCount = ''
|
||||
const g = groups.find(_g => _g.name === label)
|
||||
if (g) peersCount = ` - ${g.peers_count || 0} ${(!g.peers_count || parseInt(g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{label}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const getExistingAndToCreateGroupsLists = (groupNameList: string[]): [string[], string[]] => {
|
||||
const groupIDList = groups?.filter(g => groupNameList.includes(g.name)).map(g => g.id || '') || []
|
||||
// find groups that do not yet exist (newly added by the user)
|
||||
const existingGroupsNames: string[] = groups?.map(g => g.name);
|
||||
const groupNameListToCreate = groupNameList.filter(s => !existingGroupsNames.includes(s))
|
||||
return [groupIDList, groupNameListToCreate]
|
||||
}
|
||||
|
||||
const getGroupNamesFromIDs = (groupIDList: string[]): string[] => {
|
||||
if (!groupIDList) {
|
||||
return []
|
||||
}
|
||||
|
||||
return groups?.filter(g => groupIDList.includes(g.id!)).map(g => g.name || '') || []
|
||||
}
|
||||
|
||||
const selectValidator = (obj: RuleObject, value: string[]) => {
|
||||
if (!value.length) {
|
||||
return Promise.reject(new Error("Please enter at least one group"))
|
||||
}
|
||||
|
||||
return selectValidatorEmptyStrings(obj,value)
|
||||
}
|
||||
|
||||
const selectValidatorEmptyStrings = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = []
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v)
|
||||
}
|
||||
})
|
||||
|
||||
if (hasSpaceNamed.length) {
|
||||
return Promise.reject(new Error("Group names with just spaces are not allowed"))
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (groupTagFilterAll) {
|
||||
setTagGroups(groups?.filter(g => g.name != "All").map(g => g.name) || [])
|
||||
} else {
|
||||
setTagGroups(groups?.map(g => g.name) || [])
|
||||
}
|
||||
}, [groups])
|
||||
|
||||
return {
|
||||
tagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
selectedTagGroups,
|
||||
setGroupTagFilterAll,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidator,
|
||||
selectValidatorEmptyStrings
|
||||
}
|
||||
}
|
||||
24
src/utils/pageSize.tsx
Normal file
24
src/utils/pageSize.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import {useState} from "react";
|
||||
|
||||
export const usePageSizeHelpers = () => {
|
||||
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
const onChangePageSize = (value: string) => {
|
||||
setPageSize(parseInt(value.toString()))
|
||||
}
|
||||
|
||||
const pageSizeOptions = [
|
||||
{label: "10", value: "10"},
|
||||
{label: "25", value: "25"},
|
||||
{label: "50", value: "50"},
|
||||
{label: "100", value: "100"},
|
||||
{label: "1000", value: "1000"}
|
||||
]
|
||||
|
||||
return {
|
||||
onChangePageSize,
|
||||
pageSize,
|
||||
pageSizeOptions
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Peer, PeerNameToIP, PeerIPToName} from "../store/peer/types";
|
||||
import {Peer, PeerIPToID, PeerIPToName, PeerNameToIP} from "../store/peer/types";
|
||||
import {Route} from "../store/route/types";
|
||||
|
||||
export const routePeerSeparator = " - "
|
||||
@@ -7,22 +7,26 @@ export const masqueradeDisabledMSG = "Enabling this option hides other NetBird n
|
||||
|
||||
export const masqueradeEnabledMSG = "Disabling this option stops hiding all traffic coming from other NetBird peers behind the routing peer local address when accessing the target Network CIDR. You will need to configure routes for your NetBird network pointing to your routing peer on your local routers or other devices."
|
||||
|
||||
export const peerToPeerIP = (name:string,ip:string):string => {
|
||||
export const peerToPeerIP = (name: string, ip: string): string => {
|
||||
return name + routePeerSeparator + ip
|
||||
}
|
||||
|
||||
export const initPeerMaps = (peers:Peer[]): [PeerNameToIP, PeerIPToName] => {
|
||||
export const initPeerMaps = (peers: Peer[]): [PeerNameToIP, PeerIPToName, PeerIPToID] => {
|
||||
let peerNameToIP = {} as PeerNameToIP
|
||||
let peerIPToName = {} as PeerIPToName
|
||||
peers.forEach((p) =>{
|
||||
let peerIPToID = {} as PeerIPToID
|
||||
peers.forEach((p) => {
|
||||
peerNameToIP[p.name] = p.ip
|
||||
peerIPToName[p.ip] = p.name
|
||||
peerIPToID[p.ip] = p.id ? p.id : ""
|
||||
})
|
||||
return [ peerNameToIP, peerIPToName]
|
||||
return [peerNameToIP, peerIPToName, peerIPToID]
|
||||
}
|
||||
|
||||
export interface RouteDataTable extends Route {
|
||||
key: string;
|
||||
peer_ip: string;
|
||||
peer_name: string;
|
||||
}
|
||||
|
||||
export interface GroupedDataTable {
|
||||
@@ -34,38 +38,47 @@ export interface GroupedDataTable {
|
||||
description: string
|
||||
routesCount: number
|
||||
groupedRoutes: RouteDataTable[]
|
||||
routesGroups: string[]
|
||||
}
|
||||
|
||||
export const transformDataTable = (d:Route[],peerIPToName:PeerIPToName):RouteDataTable[] => {
|
||||
return d.map(p => {
|
||||
export const transformDataTable = (routes: Route[], peers: Peer[]): RouteDataTable[] => {
|
||||
|
||||
let peerMap = Object.fromEntries(peers.map(p => [p.id, p]));
|
||||
return routes.map(route => {
|
||||
return {
|
||||
key: p.id,
|
||||
...p,
|
||||
peer: peerIPToName[p.peer] ? peerIPToName[p.peer] : p.peer,
|
||||
key: route.id,
|
||||
...route,
|
||||
peer: route.peer,
|
||||
peer_ip: peerMap[route.peer] ? peerMap[route.peer].ip : route.peer,
|
||||
peer_name: peerMap[route.peer] ? peerMap[route.peer].name : route.peer,
|
||||
} as RouteDataTable
|
||||
})
|
||||
}
|
||||
|
||||
export const transformGroupedDataTable = (routes:Route[],peerIPToName:PeerIPToName):GroupedDataTable[] => {
|
||||
export const transformGroupedDataTable = (routes: Route[], peers: Peer[]): GroupedDataTable[] => {
|
||||
let keySet = new Set(routes.map(r => {
|
||||
return r.network_id + r.network
|
||||
}))
|
||||
|
||||
let groupedRoutes:GroupedDataTable[] = []
|
||||
let groupedRoutes: GroupedDataTable[] = []
|
||||
|
||||
keySet.forEach((p) => {
|
||||
let hasEnabled = false
|
||||
let lastRoute:Route
|
||||
let listedRoutes:Route[] = []
|
||||
let lastRoute: Route
|
||||
let listedRoutes: Route[] = []
|
||||
let groupList: string[] = []
|
||||
routes.forEach((r) => {
|
||||
if ( p === r.network_id + r.network ) {
|
||||
if (p === r.network_id + r.network) {
|
||||
lastRoute = r
|
||||
if (r.enabled) {
|
||||
hasEnabled = true
|
||||
}
|
||||
listedRoutes.push(r)
|
||||
groupList = groupList.concat(r.groups)
|
||||
}
|
||||
})
|
||||
let groupDataTableRoutes = transformDataTable(listedRoutes,peerIPToName)
|
||||
groupList = groupList.filter((value, index, arrary) => arrary.indexOf(value) === index)
|
||||
let groupDataTableRoutes = transformDataTable(listedRoutes, peers)
|
||||
groupedRoutes.push({
|
||||
key: p.toString(),
|
||||
network_id: lastRoute!.network_id,
|
||||
@@ -75,6 +88,7 @@ export const transformGroupedDataTable = (routes:Route[],peerIPToName:PeerIPToNa
|
||||
enabled: hasEnabled,
|
||||
routesCount: groupDataTableRoutes.length,
|
||||
groupedRoutes: groupDataTableRoutes,
|
||||
routesGroups: groupList,
|
||||
})
|
||||
})
|
||||
return groupedRoutes
|
||||
|
||||
71
src/utils/token.ts
Normal file
71
src/utils/token.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import {useOidcAccessToken, useOidcIdToken} from "@axa-fr/react-oidc";
|
||||
import {createRef, useEffect} from "react";
|
||||
|
||||
function sleep(ms : number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function b64DecodeUnicode(str: string): string {
|
||||
// See https://www.rfc-editor.org/rfc/rfc7515.txt, Appendix C
|
||||
str = str.replace('-', '+');
|
||||
str = str.replace('_', '/');
|
||||
switch (str.length % 4) {
|
||||
case 0: break;
|
||||
case 2: str += '=='; break;
|
||||
case 3: str += '='; break;
|
||||
}
|
||||
return decodeURIComponent(Array.prototype.map.call(atob(str), (c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join(''));
|
||||
}
|
||||
|
||||
function parseJwt(token:string) {
|
||||
return JSON.parse(b64DecodeUnicode(token.split('.')[1].replace('-', '+').replace('_', '/')))
|
||||
}
|
||||
|
||||
function isTokenValid(token:string) {
|
||||
let tokenPayload = parseJwt(token)
|
||||
return tokenPayload && (tokenPayload.exp * 1000) > (Date.now() - 5000)
|
||||
}
|
||||
|
||||
// latestToken as global allows for use of the latest state value across calls
|
||||
let latestToken:string
|
||||
|
||||
|
||||
// hook that returns a getAccessTokenSilently function that returns an access token promise,
|
||||
// waiting for renewal if it was expired
|
||||
export const useGetTokenSilently = () => {
|
||||
const getTokenSilently = async (): Promise<string> => {
|
||||
let attempt = 0
|
||||
while (!isTokenValid(latestToken) && attempt < 15){
|
||||
attempt++
|
||||
await sleep(500)
|
||||
}
|
||||
|
||||
return latestToken
|
||||
};
|
||||
|
||||
return {getTokenSilently}
|
||||
}
|
||||
|
||||
export const useTokenSource = (source:string) => {
|
||||
const {idToken} = useOidcIdToken()
|
||||
const {accessToken} = useOidcAccessToken()
|
||||
|
||||
if (source.toLowerCase() == "idtoken") {
|
||||
latestToken = idToken
|
||||
} else {
|
||||
latestToken = accessToken
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// defaults to access token(current token) if no id token was specified
|
||||
if (source.toLowerCase() != "idtoken") {
|
||||
latestToken = accessToken
|
||||
}
|
||||
}, [accessToken])
|
||||
|
||||
useEffect(() => {
|
||||
if (source.toLowerCase() == "idtoken") {
|
||||
latestToken = idToken
|
||||
}
|
||||
}, [idToken])
|
||||
}
|
||||
@@ -1,9 +1,23 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Button, Card,
|
||||
Col, Dropdown, Input, Menu, message, Modal, Popover, Radio, RadioChangeEvent,
|
||||
Row, Select, Space, Table, Tag, Tooltip,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Dropdown,
|
||||
Input,
|
||||
Menu,
|
||||
message,
|
||||
Modal,
|
||||
Popover,
|
||||
Radio,
|
||||
RadioChangeEvent,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from "antd";
|
||||
import {Container} from "../components/Container";
|
||||
@@ -13,7 +27,7 @@ import {Rule} from "../store/rule/types";
|
||||
import {actions as ruleActions} from "../store/rule";
|
||||
import {actions as groupActions} from "../store/group";
|
||||
import {filter, sortBy} from "lodash";
|
||||
import {CloseOutlined, ExclamationCircleOutlined} from "@ant-design/icons";
|
||||
import {CloseOutlined, EllipsisOutlined, ExclamationCircleOutlined} from "@ant-design/icons";
|
||||
import bidirect from '../assets/direct_bi.svg';
|
||||
import inbound from '../assets/direct_in.svg';
|
||||
import outbound from '../assets/direct_out.svg';
|
||||
@@ -21,10 +35,13 @@ import AccessControlNew from "../components/AccessControlNew";
|
||||
import {Group} from "../store/group/types";
|
||||
import AccessControlModalGroups from "../components/AccessControlModalGroups";
|
||||
import tableSpin from "../components/Spin";
|
||||
import {useOidcAccessToken} from '@axa-fr/react-oidc';
|
||||
const { Title, Paragraph } = Typography;
|
||||
const { Column } = Table;
|
||||
const { confirm } = Modal;
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {usePageSizeHelpers} from "../utils/pageSize";
|
||||
import {PeerDataTable} from "../store/peer/types";
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
const {Column} = Table;
|
||||
const {confirm} = Modal;
|
||||
|
||||
interface RuleDataTable extends Rule {
|
||||
key: string;
|
||||
@@ -41,7 +58,8 @@ interface GroupsToShow {
|
||||
}
|
||||
|
||||
export const AccessControl = () => {
|
||||
const {accessToken} = useOidcAccessToken()
|
||||
const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const rules = useSelector((state: RootState) => state.rule.data);
|
||||
@@ -53,19 +71,15 @@ export const AccessControl = () => {
|
||||
const [showTutorial, setShowTutorial] = useState(true)
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [optionAllEnable, setOptionAllEnable] = useState('enabled');
|
||||
const [pageSize, setPageSize] = useState(5);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [dataTable, setDataTable] = useState([] as RuleDataTable[]);
|
||||
const [ruleToAction, setRuleToAction] = useState(null as RuleDataTable | null);
|
||||
const [groupsToShow, setGroupsToShow] = useState({} as GroupsToShow)
|
||||
const setupNewRuleVisible = useSelector((state: RootState) => state.rule.setupNewRuleVisible);
|
||||
const [groupPopupVisible, setGroupPopupVisible] = useState("")
|
||||
|
||||
const pageSizeOptions = [
|
||||
{label: "5", value: "5"},
|
||||
{label: "10", value: "10"},
|
||||
{label: "15", value: "15"}
|
||||
]
|
||||
|
||||
const optionsAllEnabled = [{label: 'Enabled', value: 'enabled'},{label: 'All', value: 'all'}]
|
||||
const optionsAllEnabled = [{label: 'Enabled', value: 'enabled'}, {label: 'All', value: 'all'}]
|
||||
|
||||
const itemsMenuAction = [
|
||||
{
|
||||
@@ -81,17 +95,17 @@ export const AccessControl = () => {
|
||||
label: (<Button type="text" block onClick={() => showConfirmDelete()}>Delete</Button>)
|
||||
}
|
||||
]
|
||||
const actionsMenu = (<Menu items={itemsMenuAction} ></Menu>)
|
||||
const actionsMenu = (<Menu items={itemsMenuAction}></Menu>)
|
||||
|
||||
const getSourceDestinationLabel = (data:Group[]):string => {
|
||||
const getSourceDestinationLabel = (data: Group[]): string => {
|
||||
return (!data) ? "No group" : (data.length > 1) ? `${data.length} Groups` : (data.length === 1) ? data[0].name : "No group"
|
||||
}
|
||||
|
||||
const isShowTutorial = (rules:Rule[]):boolean => {
|
||||
const isShowTutorial = (rules: Rule[]): boolean => {
|
||||
return (!rules.length || (rules.length === 1 && rules[0].name === "Default"))
|
||||
}
|
||||
|
||||
const transformDataTable = (d:Rule[]):RuleDataTable[] => {
|
||||
const transformDataTable = (d: Rule[]): RuleDataTable[] => {
|
||||
return d.map(p => {
|
||||
const sourceLabel = getSourceDestinationLabel(p.sources as Group[])
|
||||
const destinationLabel = getSourceDestinationLabel(p.destinations as Group[])
|
||||
@@ -106,47 +120,66 @@ export const AccessControl = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(ruleActions.getRules.request({getAccessTokenSilently:accessToken, payload: null}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently:accessToken, payload: null}));
|
||||
dispatch(ruleActions.getRules.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setShowTutorial(isShowTutorial(rules))
|
||||
setDataTable(sortBy(transformDataTable(filterDataTable()), "name"))
|
||||
if (failed) {
|
||||
setShowTutorial(false)
|
||||
} else {
|
||||
setShowTutorial(isShowTutorial(rules))
|
||||
setDataTable(sortBy(transformDataTable(filterDataTable()), "name"))
|
||||
}
|
||||
}, [rules])
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}, [textToSearch, optionAllEnable])
|
||||
|
||||
const styleNotification = { marginTop: 85 }
|
||||
const styleNotification = {marginTop: 85}
|
||||
|
||||
const saveKey = 'saving';
|
||||
useEffect(() => {
|
||||
if (savedRule.loading) {
|
||||
message.loading({ content: 'Saving...', key: saveKey, duration: 0, style: styleNotification })
|
||||
message.loading({content: 'Saving...', key: saveKey, duration: 0, style: styleNotification})
|
||||
} else if (savedRule.success) {
|
||||
message.success({ content: 'Rule has been successfully saved.', key: saveKey, duration: 2, style: styleNotification });
|
||||
message.success({
|
||||
content: 'Rule has been successfully saved.',
|
||||
key: saveKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(ruleActions.setSetupNewRuleVisible(false))
|
||||
dispatch(ruleActions.setSavedRule({ ...savedRule, success: false }))
|
||||
dispatch(ruleActions.setSavedRule({...savedRule, success: false}))
|
||||
dispatch(ruleActions.resetSavedRule(null))
|
||||
} else if (savedRule.error) {
|
||||
message.error({ content: 'Failed to update rule. You might not have enough permissions.', key: saveKey, duration: 2, style: styleNotification });
|
||||
dispatch(ruleActions.setSavedRule({ ...savedRule, error: null }))
|
||||
message.error({
|
||||
content: 'Failed to update rule. You might not have enough permissions.',
|
||||
key: saveKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(ruleActions.setSavedRule({...savedRule, error: null}))
|
||||
dispatch(ruleActions.resetSavedRule(null))
|
||||
}
|
||||
}, [savedRule])
|
||||
|
||||
const deleteKey = 'deleting';
|
||||
useEffect(() => {
|
||||
const style = { marginTop: 85 }
|
||||
const style = {marginTop: 85}
|
||||
if (deletedRule.loading) {
|
||||
message.loading({ content: 'Deleting...', key: deleteKey, style })
|
||||
message.loading({content: 'Deleting...', key: deleteKey, style})
|
||||
} else if (deletedRule.success) {
|
||||
message.success({ content: 'Rule has been successfully disabled.', key: deleteKey, duration: 2, style })
|
||||
message.success({content: 'Rule has been successfully disabled.', key: deleteKey, duration: 2, style})
|
||||
dispatch(ruleActions.resetDeletedRule(null))
|
||||
} else if (deletedRule.error) {
|
||||
message.error({ content: 'Failed to remove rule. You might not have enough permissions.', key: deleteKey, duration: 2, style })
|
||||
message.error({
|
||||
content: 'Failed to remove rule. You might not have enough permissions.',
|
||||
key: deleteKey,
|
||||
duration: 2,
|
||||
style
|
||||
})
|
||||
dispatch(ruleActions.resetDeletedRule(null))
|
||||
}
|
||||
}, [deletedRule])
|
||||
@@ -160,29 +193,25 @@ export const AccessControl = () => {
|
||||
setDataTable(transformDataTable(data))
|
||||
}
|
||||
|
||||
const onChangeAllEnabled = ({ target: { value } }: RadioChangeEvent) => {
|
||||
const onChangeAllEnabled = ({target: {value}}: RadioChangeEvent) => {
|
||||
setOptionAllEnable(value)
|
||||
}
|
||||
|
||||
const onChangePageSize = (value: string) => {
|
||||
setPageSize(parseInt(value.toString()))
|
||||
}
|
||||
|
||||
const showConfirmDelete = () => {
|
||||
let name = ruleToAction ? ruleToAction.name : '';
|
||||
confirm({
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
title: "Delete rule \"" + name + "\"",
|
||||
width: 600,
|
||||
content: <Space direction="vertical" size="small">
|
||||
{ruleToAction &&
|
||||
<>
|
||||
<Title level={5}>Delete rule "{ruleToAction ? ruleToAction.name : ''}"</Title>
|
||||
<Paragraph>Are you sure you want to delete peer from your account?</Paragraph>
|
||||
</>
|
||||
}
|
||||
<Paragraph>Are you sure you want to delete this rule from your account?</Paragraph>
|
||||
</Space>,
|
||||
okType: 'danger',
|
||||
onOk() {
|
||||
dispatch(ruleActions.deleteRule.request({getAccessTokenSilently:accessToken, payload: ruleToAction?.id || ''}));
|
||||
dispatch(ruleActions.deleteRule.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: ruleToAction?.id || ''
|
||||
}));
|
||||
},
|
||||
onCancel() {
|
||||
setRuleToAction(null);
|
||||
@@ -192,7 +221,7 @@ export const AccessControl = () => {
|
||||
|
||||
const showConfirmDeactivate = () => {
|
||||
confirm({
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
width: 600,
|
||||
content: <Space direction="vertical" size="small">
|
||||
{ruleToAction &&
|
||||
@@ -212,13 +241,13 @@ export const AccessControl = () => {
|
||||
});
|
||||
}
|
||||
|
||||
const filterDataTable = ():Rule[] => {
|
||||
const filterDataTable = (): Rule[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let f:Rule[] = filter(rules, (f:Rule) =>
|
||||
let f: Rule[] = filter(rules, (f: Rule) =>
|
||||
(f.name.toLowerCase().includes(t) || f.description.toLowerCase().includes(t) || t === "")
|
||||
) as Rule[]
|
||||
if (optionAllEnable !== "all") {
|
||||
f = filter(f, (f:Rule) => !f.disabled)
|
||||
f = filter(f, (f: Rule) => !f.disabled)
|
||||
}
|
||||
return f
|
||||
}
|
||||
@@ -261,7 +290,7 @@ export const AccessControl = () => {
|
||||
} as Rule))
|
||||
}
|
||||
|
||||
const toggleModalGroups = (title:string, groups:Group[] | string[] | null, modalVisible:boolean) => {
|
||||
const toggleModalGroups = (title: string, groups: Group[] | string[] | null, modalVisible: boolean) => {
|
||||
setGroupsToShow({
|
||||
title,
|
||||
groups,
|
||||
@@ -269,40 +298,64 @@ export const AccessControl = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const renderPopoverGroups = (label: string, groups:Group[] | string[] | null, rule: RuleDataTable) => {
|
||||
useEffect(() => {
|
||||
if (setupNewRuleVisible) {
|
||||
setGroupPopupVisible("")
|
||||
}
|
||||
}, [setupNewRuleVisible])
|
||||
|
||||
const onPopoverVisibleChange = (b: boolean, key: string) => {
|
||||
if (setupNewRuleVisible) {
|
||||
setGroupPopupVisible("")
|
||||
} else {
|
||||
if (b) {
|
||||
setGroupPopupVisible(key)
|
||||
} else {
|
||||
setGroupPopupVisible("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const renderPopoverGroups = (label: string, groups: Group[] | string[] | null, rule: RuleDataTable) => {
|
||||
const content = groups?.map((g, i) => {
|
||||
const _g = g as Group
|
||||
const peersCount = ` - ${_g.peers_count || 0} ${(!_g.peers_count || parseInt(_g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<div key={i}>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{ marginRight: 3 }}
|
||||
>
|
||||
<strong>{_g.name}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</div>
|
||||
<div key={i}>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{_g.name}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
const mainContent = (<Space direction="vertical">{content}</Space>)
|
||||
return (
|
||||
<Popover content={<Space direction="vertical">{content}</Space>} title={null}>
|
||||
<Popover
|
||||
onOpenChange={(b: boolean) => onPopoverVisibleChange(b, rule.key)}
|
||||
open={groupPopupVisible === rule.key}
|
||||
content={mainContent}
|
||||
title={null}>
|
||||
<Button type="link" onClick={() => setRuleAndView(rule)}>{label}</Button>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
return(
|
||||
return (
|
||||
<>
|
||||
<Container className="container-main">
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Access Control</Title>
|
||||
<Paragraph>Access rules help you manage access permissions in your organisation.</Paragraph>
|
||||
<Space direction="vertical" size="large" style={{ display: 'flex' }}>
|
||||
<Space direction="vertical" size="large" style={{display: 'flex'}}>
|
||||
<Row gutter={[16, 24]}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
|
||||
<Input allowClear value={textToSearch} onPressEnter={searchDataTable} placeholder="Search..." onChange={onChangeTextToSearch} />
|
||||
<Input allowClear value={textToSearch} onPressEnter={searchDataTable}
|
||||
placeholder="Search..." onChange={onChangeTextToSearch}/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
|
||||
<Space size="middle">
|
||||
@@ -313,7 +366,8 @@ export const AccessControl = () => {
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
/>
|
||||
<Select value={pageSize.toString()} options={pageSizeOptions} onChange={onChangePageSize} className="select-rows-per-page-en"/>
|
||||
<Select value={pageSize.toString()} options={pageSizeOptions}
|
||||
onChange={onChangePageSize} className="select-rows-per-page-en"/>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24}
|
||||
@@ -324,13 +378,15 @@ export const AccessControl = () => {
|
||||
xxl={5} span={5}>
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
<Button type="primary" disabled={savedRule.loading} onClick={onClickAddNewRule}>Add Rule</Button>
|
||||
<Button type="primary" disabled={savedRule.loading}
|
||||
onClick={onClickAddNewRule}>Add Rule</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
{failed &&
|
||||
<Alert message={failed.code} description={failed.message} type="error" showIcon closable/>
|
||||
<Alert message={failed.message} description={failed.data ? failed.data.message : " "} type="error" showIcon
|
||||
closable/>
|
||||
}
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Table
|
||||
@@ -353,24 +409,27 @@ export const AccessControl = () => {
|
||||
defaultSortOrder='ascend'
|
||||
render={(text, record, index) => {
|
||||
const desc = (record as RuleDataTable).description.trim()
|
||||
return <Tooltip title={desc !== "" ? desc : "no description"} arrowPointAtCenter>
|
||||
<span onClick={() => setRuleAndView(record as RuleDataTable)} className="tooltip-label">{text}</span>
|
||||
return <Tooltip title={desc !== "" ? desc : "no description"}
|
||||
arrowPointAtCenter>
|
||||
<span onClick={() => setRuleAndView(record as RuleDataTable)}
|
||||
className="tooltip-label"><Text strong>{text}</Text></span>
|
||||
</Tooltip>
|
||||
}}
|
||||
/>
|
||||
<Column title="Status" dataIndex="disabled"
|
||||
render={(text:Boolean, record:RuleDataTable, index) => {
|
||||
return text ? <Tag color="red">disabled</Tag> : <Tag color="green">enabled</Tag>
|
||||
render={(text: Boolean, record: RuleDataTable, index) => {
|
||||
return text ? <Tag color="red">disabled</Tag> :
|
||||
<Tag color="green">enabled</Tag>
|
||||
}}
|
||||
/>
|
||||
<Column title="Sources" dataIndex="sourceLabel"
|
||||
render={(text, record:RuleDataTable, index) => {
|
||||
render={(text, record: RuleDataTable, index) => {
|
||||
//return <Button type="link" onClick={() => toggleModalGroups(`${record.Name} - Sources`, record.Source, true)}>{text}</Button>
|
||||
return renderPopoverGroups(text, record.sources,record as RuleDataTable)
|
||||
return renderPopoverGroups(text, record.sources, record as RuleDataTable)
|
||||
}}
|
||||
/>
|
||||
<Column title="Direction" dataIndex="flow"
|
||||
render={(text, record:RuleDataTable, index) => {
|
||||
render={(text, record: RuleDataTable, index) => {
|
||||
const s = {minWidth: 50, textAlign: "center"} as React.CSSProperties
|
||||
if (text === "bidirect")
|
||||
return <Tag color="processing" style={s}><img src={bidirect}/></Tag>
|
||||
@@ -379,22 +438,29 @@ export const AccessControl = () => {
|
||||
} else if (text === "destToSrc") {
|
||||
return <Tag color="green" style={s}><img src={inbound}/></Tag>
|
||||
}
|
||||
return <Tag color="red" style={s}><CloseOutlined /></Tag>
|
||||
return <Tag color="red" style={s}><CloseOutlined/></Tag>
|
||||
}}
|
||||
/>
|
||||
<Column title="Destinations" dataIndex="destinationLabel"
|
||||
render={(text, record:RuleDataTable, index) => {
|
||||
render={(text, record: RuleDataTable, index) => {
|
||||
//return <Button type="link" onClick={() => toggleModalGroups(`${record.name} - Destinations`, record.destinations, true)}>{text}</Button>
|
||||
return renderPopoverGroups(text, record.destinations,record as RuleDataTable)
|
||||
return renderPopoverGroups(text, record.destinations, record as RuleDataTable)
|
||||
}}
|
||||
/>
|
||||
<Column title="" align="center"
|
||||
render={(text, record, index) => {
|
||||
if (deletedRule.loading || savedRule.loading) return <></>
|
||||
return <Dropdown.Button type="text" overlay={actionsMenu} trigger={["click"]}
|
||||
onVisibleChange={visible => {
|
||||
if (visible) setRuleToAction(record as RuleDataTable)
|
||||
}}></Dropdown.Button>
|
||||
return (
|
||||
<Dropdown trigger={["click"]} overlay={actionsMenu} onOpenChange={visible => {
|
||||
if (visible) setRuleToAction(record as RuleDataTable)
|
||||
}}>
|
||||
<Button type="text">
|
||||
<Space>
|
||||
<EllipsisOutlined />
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
@@ -409,7 +475,9 @@ export const AccessControl = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<AccessControlModalGroups data={groupsToShow.groups} title={groupsToShow.title} visible={groupsToShow.modalVisible} onCancel={() => toggleModalGroups("", [], false)}/>
|
||||
<AccessControlModalGroups data={groupsToShow.groups} title={groupsToShow.title}
|
||||
visible={groupsToShow.modalVisible}
|
||||
onCancel={() => toggleModalGroups("", [], false)}/>
|
||||
<AccessControlNew/>
|
||||
</>
|
||||
)
|
||||
|
||||
309
src/views/Activity.tsx
Normal file
309
src/views/Activity.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as eventActions} from '../store/event';
|
||||
import {Container} from "../components/Container";
|
||||
import {Alert, Button, Card, Col, Input, Row, Select, Space, Table, Typography,} from "antd";
|
||||
import {Event} from "../store/event/types";
|
||||
import {filter} from "lodash";
|
||||
import tableSpin from "../components/Spin";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {useOidcUser} from "@axa-fr/react-oidc";
|
||||
import {capitalize, formatDateTime} from "../utils/common";
|
||||
import {User} from "../store/user/types";
|
||||
import {usePageSizeHelpers} from "../utils/pageSize";
|
||||
import {QuestionCircleFilled} from "@ant-design/icons";
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
const {Column} = Table;
|
||||
|
||||
interface EventDataTable extends Event {
|
||||
}
|
||||
|
||||
export const Activity = () => {
|
||||
const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const {oidcUser} = useOidcUser();
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const events = useSelector((state: RootState) => state.event.data);
|
||||
const failed = useSelector((state: RootState) => state.event.failed);
|
||||
const loading = useSelector((state: RootState) => state.event.loading);
|
||||
const users = useSelector((state: RootState) => state.user.data);
|
||||
const setupKeys = useSelector((state: RootState) => state.setupKey.data);
|
||||
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [dataTable, setDataTable] = useState([] as EventDataTable[]);
|
||||
|
||||
|
||||
const transformDataTable = (d: Event[]): EventDataTable[] => {
|
||||
return d.map(p => ({key: p.id, ...p} as EventDataTable))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(eventActions.getEvents.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(events))
|
||||
}, [events])
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}, [textToSearch])
|
||||
|
||||
const filterDataTable = (): Event[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let usrsMatch: User[] = filter(users, (u: User) => (u.name)?.toLowerCase().includes(t) || (u.email)?.toLowerCase().includes(t)) as User[]
|
||||
let f: Event[] = filter(events, (f: Event) =>
|
||||
((f.activity || f.id).toLowerCase().includes(t) || t === "" || usrsMatch.find(u => u.id === f.initiator_id))
|
||||
) as Event[]
|
||||
return f
|
||||
}
|
||||
|
||||
const onChangeTextToSearch = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setTextToSearch(e.target.value)
|
||||
};
|
||||
|
||||
const searchDataTable = () => {
|
||||
const data = filterDataTable()
|
||||
setDataTable(transformDataTable(data))
|
||||
}
|
||||
|
||||
const getActivityRow = (objectType: string, name:string,text:string) => {
|
||||
return <Row> <Text>{objectType} <Text type="secondary">{name}</Text> {text}</Text> </Row>
|
||||
}
|
||||
|
||||
const renderActivity = (event: EventDataTable) => {
|
||||
let body = <Text>{event.activity}</Text>
|
||||
switch (event.activity_code) {
|
||||
case "peer.group.add":
|
||||
return getActivityRow("Group", event.meta.group,"added to peer")
|
||||
case "peer.group.delete":
|
||||
return getActivityRow("Group", event.meta.group,"removed from peer")
|
||||
case "user.group.add":
|
||||
return getActivityRow("Group", event.meta.group,"added to user")
|
||||
case "user.group.delete":
|
||||
return getActivityRow("Group", event.meta.group,"removed from user")
|
||||
case "setupkey.group.add":
|
||||
return getActivityRow("Group", event.meta.group,"added to setup key")
|
||||
case "setupkey.group.delete":
|
||||
return getActivityRow("Group", event.meta.group,"removed setup key")
|
||||
case "dns.setting.disabled.management.group.add":
|
||||
return getActivityRow("Group", event.meta.group,"added to disabled management DNS setting")
|
||||
case "dns.setting.disabled.management.group.delete":
|
||||
return getActivityRow("Group", event.meta.group,"removed from disabled management DNS setting")
|
||||
case "personal.access.token.create":
|
||||
return getActivityRow("Personal access token", event.meta.name,"added to user")
|
||||
case "personal.access.token.delete":
|
||||
return getActivityRow("Personal access token", event.meta.name,"removed from user")
|
||||
}
|
||||
return body
|
||||
}
|
||||
const renderInitiator = (event: EventDataTable) => {
|
||||
let body = <></>
|
||||
const user = users?.find(u => u.id === event.initiator_id)
|
||||
switch (event.activity_code) {
|
||||
case "setupkey.peer.add":
|
||||
const key = setupKeys?.find(k => k.id === event.initiator_id)
|
||||
if (key) {
|
||||
body = <span style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}>
|
||||
<Row> <Text>{key.name}</Text> </Row>
|
||||
<Row> <Text type="secondary">Setup Key</Text> </Row>
|
||||
</span>
|
||||
}
|
||||
break
|
||||
default:
|
||||
if (user) {
|
||||
body = <span style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}>
|
||||
<Row> <Text>{user.name ? user.name : user.id}</Text> </Row>
|
||||
<Row> <Text type="secondary">{user.email ? user.email : user.is_service_user ? "Service User" : "User"}</Text> </Row>
|
||||
</span>
|
||||
return body
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
const renderMultiRowSpan = (primaryRowText:string,secondaryRowText:string) => {
|
||||
return <span style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}>
|
||||
<Row> <Text>{primaryRowText}</Text> </Row>
|
||||
<Row> <Text type="secondary">{secondaryRowText}</Text> </Row>
|
||||
</span>
|
||||
}
|
||||
|
||||
const renderTarget = (event: EventDataTable) => {
|
||||
if (event.activity_code === "account.create" || event.activity_code === "user.join") {
|
||||
return "-"
|
||||
}
|
||||
const user = users?.find(u => u.id === event.target_id)
|
||||
switch (event.activity_code) {
|
||||
case "account.create":
|
||||
case "user.join":
|
||||
return "-"
|
||||
case "rule.add":
|
||||
case "rule.delete":
|
||||
case "rule.update":
|
||||
return renderMultiRowSpan(event.meta.name,"Rule")
|
||||
case "policy.add":
|
||||
case "policy.delete":
|
||||
case "policy.update":
|
||||
return renderMultiRowSpan(event.meta.name, "Policy")
|
||||
case "setupkey.add":
|
||||
case "setupkey.revoke":
|
||||
case "setupkey.update":
|
||||
case "setupkey.overuse":
|
||||
let cType:string
|
||||
cType = capitalize(event.meta.type)
|
||||
return renderMultiRowSpan(event.meta.name,cType+" setup key "+event.meta.key)
|
||||
case "group.add":
|
||||
case "group.update":
|
||||
return renderMultiRowSpan(event.meta.name,"Group")
|
||||
case "nameserver.group.add":
|
||||
case "nameserver.group.update":
|
||||
case "nameserver.group.delete":
|
||||
return renderMultiRowSpan(event.meta.name,"Nameserver group")
|
||||
case "setupkey.peer.add":
|
||||
case "user.peer.add":
|
||||
case "user.peer.delete":
|
||||
case "peer.ssh.enable":
|
||||
case "peer.ssh.disable":
|
||||
case "peer.rename":
|
||||
case "peer.login.expiration.disable":
|
||||
case "peer.login.expiration.enable":
|
||||
return renderMultiRowSpan(event.meta.fqdn,event.meta.ip)
|
||||
case "route.add":
|
||||
case "route.delete":
|
||||
case "route.update":
|
||||
return renderMultiRowSpan(event.meta.name, "Route for range " + event.meta.network_range)
|
||||
case "user.group.add":
|
||||
case "user.group.delete":
|
||||
case "user.role.update":
|
||||
if (user) {
|
||||
return renderMultiRowSpan((user.name ? user.name : user.id),user.email ? user.email : user.is_service_user ? "Service User" : "User")
|
||||
}
|
||||
if (event.meta.user_name) {
|
||||
return renderMultiRowSpan(event.meta.user_name, event.meta.is_service_user ? "Service User" : "User")
|
||||
}
|
||||
return "-"
|
||||
case "setupkey.group.add":
|
||||
case "setupkey.group.delete":
|
||||
return renderMultiRowSpan(event.meta.setupkey,"Setup Key")
|
||||
case "peer.group.add":
|
||||
case "peer.group.delete":
|
||||
return renderMultiRowSpan(event.meta.peer_fqdn,event.meta.peer_ip)
|
||||
case "dns.setting.disabled.management.group.add":
|
||||
case "dns.setting.disabled.management.group.delete":
|
||||
case "account.setting.peer.login.expiration.enable":
|
||||
case "account.setting.peer.login.expiration.disable":
|
||||
case "account.setting.peer.login.expiration.update":
|
||||
return renderMultiRowSpan("","System setting")
|
||||
case "personal.access.token.create":
|
||||
case "personal.access.token.delete":
|
||||
if(user) {
|
||||
return renderMultiRowSpan((user.name ? user.name : user.id), user.email ? user.email : user.is_service_user ? "Service User" : "User")
|
||||
}
|
||||
if (event.meta.user_name) {
|
||||
return renderMultiRowSpan(event.meta.user_name,event.meta.is_service_user ? "Service User" : "User")
|
||||
}
|
||||
return "-"
|
||||
case "service.user.create":
|
||||
case "service.user.delete":
|
||||
return renderMultiRowSpan(event.meta.name,"Service User")
|
||||
case "user.invite":
|
||||
if (user) {
|
||||
return renderMultiRowSpan(user.name ? user.name : user.id,user.email ? user.email : "User")
|
||||
}
|
||||
break
|
||||
default:
|
||||
console.error("unknown event - missing handling", event.activity_code)
|
||||
}
|
||||
|
||||
return event.target_id
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container style={{paddingTop: "40px"}}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Activity</Title>
|
||||
<Paragraph>Here you can see all the account and network activity events</Paragraph>
|
||||
<Space direction="vertical" size="large" style={{display: 'flex'}}>
|
||||
<Row gutter={[16, 24]}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
|
||||
<Input allowClear value={textToSearch} onPressEnter={searchDataTable}
|
||||
placeholder="Search..." onChange={onChangeTextToSearch}/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
|
||||
<Space size="middle">
|
||||
<Select value={pageSize.toString()} options={pageSizeOptions}
|
||||
onChange={onChangePageSize} className="select-rows-per-page-en"/>
|
||||
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24}
|
||||
sm={24}
|
||||
md={5}
|
||||
lg={5}
|
||||
xl={5}
|
||||
xxl={5} span={5}>
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
|
||||
href="https://netbird.io/docs/how-to-guides/activity-monitoring">Learn more about activity tracking</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
{failed &&
|
||||
<Alert message={failed.message} description={failed.data ? failed.data.message : " "}
|
||||
type="error" showIcon
|
||||
closable/>
|
||||
}
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Table
|
||||
pagination={{
|
||||
pageSize,
|
||||
showSizeChanger: false,
|
||||
showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} activity events`)
|
||||
}}
|
||||
className="card-table"
|
||||
showSorterTooltip={false}
|
||||
scroll={{x: true}}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={dataTable}
|
||||
size="small"
|
||||
>
|
||||
<Column title="Timestamp" dataIndex="timestamp"
|
||||
render={(text, record, index) => {
|
||||
return formatDateTime(text)
|
||||
}}
|
||||
/>
|
||||
<Column title="Activity" dataIndex="activity"
|
||||
render={(text, record, index) => {
|
||||
return renderActivity(record as EventDataTable)
|
||||
}}
|
||||
/>
|
||||
<Column title="Initiated By" dataIndex="initiator_id"
|
||||
render={(text, record, index) => {
|
||||
return renderInitiator(record as EventDataTable)
|
||||
}}
|
||||
/>
|
||||
<Column title="Target" dataIndex="target_id"
|
||||
render={(text, record, index) => {
|
||||
return renderTarget(record as EventDataTable)
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
</Card>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Activity;
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
Tabs
|
||||
} from "antd";
|
||||
|
||||
import OtherTab from "../components/addpeer/LinuxTab";
|
||||
import UbuntuTab from "../components/addpeer/UbuntuTab";
|
||||
import MacTab from "../components/addpeer/MacTab";
|
||||
import WindowsTab from "../components/addpeer/WindowsTab";
|
||||
import OtherTab from "../components/popups/addpeer/addpeer/LinuxTab";
|
||||
import UbuntuTab from "../components/popups/addpeer/addpeer/UbuntuTab";
|
||||
import MacTab from "../components/popups/addpeer/addpeer/MacTab";
|
||||
import WindowsTab from "../components/popups/addpeer/addpeer/WindowsTab";
|
||||
const { Title, Paragraph } = Typography;
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
|
||||
78
src/views/DNS.tsx
Normal file
78
src/views/DNS.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Container} from "../components/Container";
|
||||
import {
|
||||
Col,
|
||||
Row,
|
||||
Tabs,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import type { TabsProps } from 'antd';
|
||||
import NameServerGroupUpdate from "../components/NameServerGroupUpdate";
|
||||
import Nameservers from "./Nameservers";
|
||||
import {actions as groupActions} from "../store/group";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import DNSSettingsForm from "./DNSSettings";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as dnsSettingsActions} from '../store/dns-settings';
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
|
||||
const {Title, Paragraph} = Typography;
|
||||
|
||||
export const DNS = () => {
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const {
|
||||
getGroupNamesFromIDs,
|
||||
} = useGetGroupTagHelpers()
|
||||
|
||||
const dnsSettingsData = useSelector((state: RootState) => state.dnsSettings.data)
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
}, [])
|
||||
|
||||
const nsTabKey = '1'
|
||||
const items: TabsProps['items'] = [
|
||||
{
|
||||
key: nsTabKey,
|
||||
label: 'Nameservers',
|
||||
children: <Nameservers/>,
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: 'Settings',
|
||||
children: <DNSSettingsForm/>,
|
||||
},
|
||||
]
|
||||
|
||||
const onTabClick = (key:string) => {
|
||||
if (key == nsTabKey) {
|
||||
if (!dnsSettingsData) return
|
||||
dispatch(dnsSettingsActions.setDNSSettings({
|
||||
disabled_management_groups: getGroupNamesFromIDs(dnsSettingsData.disabled_management_groups),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container style={{paddingTop: "40px"}}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Tabs
|
||||
defaultActiveKey={nsTabKey}
|
||||
items={items}
|
||||
onTabClick={onTabClick}
|
||||
animated={{ inkBar: true, tabPane: false }}
|
||||
tabPosition="top"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<NameServerGroupUpdate/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DNS;
|
||||
176
src/views/DNSSettings.tsx
Normal file
176
src/views/DNSSettings.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Form,
|
||||
message,
|
||||
Select,
|
||||
Space,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
import {actions as dnsSettingsActions} from '../store/dns-settings';
|
||||
import {DNSSettings, DNSSettingsToSave} from "../store/dns-settings/types";
|
||||
import {actions as nsGroupActions} from "../store/nameservers";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
const styleNotification = {marginTop: 85}
|
||||
|
||||
export const DNSSettingsForm = () => {
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const {
|
||||
tagRender,
|
||||
handleChangeTags,
|
||||
dropDownRender,
|
||||
optionRender,
|
||||
tagGroups,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidatorEmptyStrings
|
||||
} = useGetGroupTagHelpers()
|
||||
|
||||
const dnsSettings = useSelector((state: RootState) => state.dnsSettings.dnsSettings)
|
||||
const dnsSettingsData = useSelector((state: RootState) => state.dnsSettings.data)
|
||||
const savedDNSSettings = useSelector((state: RootState) => state.dnsSettings.savedDNSSettings)
|
||||
const loading = useSelector((state: RootState) => state.dnsSettings.loading);
|
||||
|
||||
|
||||
const [form] = Form.useForm()
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(dnsSettingsActions.getDNSSettings.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dnsSettingsData) return
|
||||
dispatch(dnsSettingsActions.setDNSSettings({
|
||||
disabled_management_groups: getGroupNamesFromIDs(dnsSettingsData.disabled_management_groups),
|
||||
}))
|
||||
}, [dnsSettingsData])
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(dnsSettings)
|
||||
}, [dnsSettings])
|
||||
|
||||
const createKey = 'saving';
|
||||
useEffect(() => {
|
||||
if (savedDNSSettings.loading) {
|
||||
message.loading({content: 'Saving...', key: createKey, duration: 0, style: styleNotification});
|
||||
} else if (savedDNSSettings.success) {
|
||||
message.success({
|
||||
content: 'DNS settings has been successfully saved.',
|
||||
key: createKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(dnsSettingsActions.setSavedDNSSettings({...savedDNSSettings, success: false}));
|
||||
dispatch(dnsSettingsActions.resetSavedDNSSettings(null))
|
||||
} else if (savedDNSSettings.error) {
|
||||
let errorMsg = "Failed to update DNS settings"
|
||||
switch (savedDNSSettings.error.statusCode) {
|
||||
case 403:
|
||||
errorMsg = "Failed to update DNS settings. You might not have enough permissions."
|
||||
break
|
||||
default:
|
||||
errorMsg = savedDNSSettings.error.data.message ? savedDNSSettings.error.data.message : errorMsg
|
||||
break
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: createKey,
|
||||
duration: 5,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(dnsSettingsActions.setSavedDNSSettings({...savedDNSSettings, error: null}));
|
||||
dispatch(nsGroupActions.resetSavedNameServerGroup(null))
|
||||
}
|
||||
}, [savedDNSSettings])
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
let dnsSettingsToSave = createDNSSettingsToSave(values)
|
||||
dispatch(dnsSettingsActions.saveDNSSettings.request({
|
||||
getAccessTokenSilently:getTokenSilently,
|
||||
payload: dnsSettingsToSave
|
||||
}))
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
let msg = "please check the fields and try again"
|
||||
if (errorInfo.errorFields) {
|
||||
msg = errorInfo.errorFields[0].errors[0]
|
||||
}
|
||||
message.error({
|
||||
content: msg,
|
||||
duration: 1,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const createDNSSettingsToSave = (values: DNSSettings): DNSSettingsToSave => {
|
||||
let [existingGroups, newGroups] = getExistingAndToCreateGroupsLists(values.disabled_management_groups)
|
||||
return {
|
||||
disabled_management_groups: existingGroups,
|
||||
groupsToCreate: newGroups
|
||||
} as DNSSettingsToSave
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paragraph>Manage your account's DNS settings</Paragraph>
|
||||
<Col>
|
||||
<Form
|
||||
name="basic"
|
||||
autoComplete="off"
|
||||
form={form}
|
||||
onFinish={handleFormSubmit}
|
||||
>
|
||||
<Space direction={"vertical"}
|
||||
style={{ display: 'flex' }}>
|
||||
<Card
|
||||
title="DNS Management"
|
||||
loading={loading}
|
||||
>
|
||||
<Form.Item
|
||||
label="Disable DNS management for these groups"
|
||||
name="disabled_management_groups"
|
||||
tooltip="Peers in these groups will have their DNS management disabled and require manual configuration for domain name resolution"
|
||||
rules={[{validator: selectValidatorEmptyStrings}]}
|
||||
>
|
||||
<Select mode="tags"
|
||||
style={{width: '100%'}}
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangeTags}
|
||||
dropdownRender={dropDownRender}
|
||||
>
|
||||
{
|
||||
tagGroups.map(m =>
|
||||
<Select.Option key={m}>{optionRender(m)}</Select.Option>
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
<Form.Item style={{ textAlign:'center' }} >
|
||||
<Button type="primary" htmlType="submit">
|
||||
Save
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Form>
|
||||
</Col>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DNSSettingsForm;
|
||||
114
src/views/ExpiresInInput.tsx
Normal file
114
src/views/ExpiresInInput.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import {Input, Select, Space} from 'antd';
|
||||
import React, {useState} from 'react';
|
||||
|
||||
export interface ExpiresInValue {
|
||||
number?: number;
|
||||
interval?: string;
|
||||
}
|
||||
|
||||
export interface SelectOption {
|
||||
key: string,
|
||||
title: string
|
||||
}
|
||||
|
||||
interface ExpiresInInputProps {
|
||||
value?: ExpiresInValue;
|
||||
onChange?: (value: ExpiresInValue) => void;
|
||||
options: SelectOption[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const secondsToExpiresIn = (expiresIn: number, availableOptions: string[]): ExpiresInValue => {
|
||||
|
||||
if (expiresIn == 0) {
|
||||
return {interval: "day", number: 0}
|
||||
}
|
||||
|
||||
let result = {interval: "hour", number: expiresIn / 3600}
|
||||
availableOptions.forEach(opt => {
|
||||
if (opt === "year" && (expiresIn % 31104000 === 0)) {
|
||||
result = {interval: "year", number: expiresIn / 31104000}
|
||||
} else if (opt === "month" && (expiresIn % 2592000 === 0)) {
|
||||
result = {interval: "month", number: expiresIn / 2592000}
|
||||
} else if (opt === "day" && (expiresIn % 86400 === 0)) {
|
||||
result = {interval: "day", number: expiresIn / 86400}
|
||||
} else if (opt === "hour" && (expiresIn % 3600 === 0)) {
|
||||
result = {interval: "hour", number: expiresIn / 3600}
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
export const expiresInToSeconds = (expiresIn: ExpiresInValue): number => {
|
||||
if (!expiresIn.number || !expiresIn.interval) {
|
||||
return 0
|
||||
}
|
||||
let multiplier = 0
|
||||
switch (expiresIn.interval.toLowerCase()) {
|
||||
case "hour":
|
||||
multiplier = 3600
|
||||
break
|
||||
case "day":
|
||||
multiplier = 24 * 3600
|
||||
break
|
||||
case "week":
|
||||
multiplier = 7 * 24 * 3600
|
||||
break
|
||||
case "month":
|
||||
multiplier = 30 * 24 * 3600
|
||||
break
|
||||
case "year":
|
||||
multiplier = 365 * 24 * 3600
|
||||
break
|
||||
default:
|
||||
multiplier = 0
|
||||
}
|
||||
|
||||
return expiresIn.number * multiplier
|
||||
|
||||
}
|
||||
|
||||
const ExpiresInInput: React.FC<ExpiresInInputProps> = ({
|
||||
value = {},
|
||||
onChange,
|
||||
options,
|
||||
disabled= false,
|
||||
}) => {
|
||||
const [number, setNumber] = useState(60);
|
||||
const [interval, setInterval] = useState("day");
|
||||
|
||||
const triggerChange = (changedValue: { number?: number; interval?: string }) => {
|
||||
onChange?.({number, interval, ...value, ...changedValue});
|
||||
};
|
||||
|
||||
const onNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newNumber = parseInt(e.target.value || '0', 10);
|
||||
setNumber(newNumber);
|
||||
triggerChange({number: newNumber});
|
||||
};
|
||||
|
||||
const onIntervalChange = (newInterval: string) => {
|
||||
setInterval(newInterval);
|
||||
triggerChange({interval: newInterval});
|
||||
};
|
||||
|
||||
return (
|
||||
<Space>
|
||||
<Input
|
||||
type="number"
|
||||
value={value.number || number}
|
||||
onChange={onNumberChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Select style={{width: "100%"}}
|
||||
value={value?.interval || interval}
|
||||
disabled={disabled}
|
||||
onChange={onIntervalChange}>
|
||||
{options.map(m =>
|
||||
<Select.Option key={m.key}>{m.title}</Select.Option>)}
|
||||
</Select>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExpiresInInput;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user