Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67fd2fcb2e | ||
|
|
f4933e45ee | ||
|
|
331dd2b429 | ||
|
|
3a8106c1e7 | ||
|
|
b595e0d6a8 | ||
|
|
33621cae5d | ||
|
|
360d807008 | ||
|
|
6f8897ffa5 | ||
|
|
75d5f804c5 | ||
|
|
f7cac02a2d | ||
|
|
ec40730cb2 | ||
|
|
7f3648861b | ||
|
|
b50464db43 | ||
|
|
1eb5ccc131 | ||
|
|
ac42a17b11 | ||
|
|
77ca3c6fde | ||
|
|
20e24b4ede | ||
|
|
7a29dac01c | ||
|
|
444e9ec44a | ||
|
|
dff0313f82 | ||
|
|
11fbfb336a | ||
|
|
4a0ae8f27d | ||
|
|
9a72d8b0c4 | ||
|
|
8e038cf242 | ||
|
|
5bd94eff56 | ||
|
|
77f065b093 | ||
|
|
a4d55cfb90 | ||
|
|
cfd4c9075b | ||
|
|
962180030a | ||
|
|
485e1e8d79 | ||
|
|
b11007b29f | ||
|
|
bce75c1ca9 | ||
|
|
0c09992b38 | ||
|
|
86b12f30d2 | ||
|
|
d3e34d8448 | ||
|
|
76083168f6 | ||
|
|
a54b3687ae | ||
|
|
25f154dc83 | ||
|
|
f3c7d877f8 | ||
|
|
7cea7e7f54 | ||
|
|
aaa351635f | ||
|
|
379ff5486e | ||
|
|
8bcd9918e2 | ||
|
|
044ccd0ce6 | ||
|
|
ab09ca3697 | ||
|
|
1644ed5dce | ||
|
|
cea459792f | ||
|
|
a402680816 | ||
|
|
e57e5b726d | ||
|
|
2c4ada0ad8 | ||
|
|
8195587c85 | ||
|
|
adf6e1e71f | ||
|
|
b733a186ae | ||
|
|
5d901470c2 | ||
|
|
29ab28847d | ||
|
|
0361825e04 | ||
|
|
2fa33ec06a | ||
|
|
c677eeaae4 |
37
README.md
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`
|
||||
|
||||
@@ -52,14 +52,12 @@ export AUTH_SUPPORTED_SCOPES=${AUTH_SUPPORTED_SCOPES:-openid profile email api o
|
||||
export NETBIRD_MGMT_API_ENDPOINT=$(echo $NETBIRD_MGMT_API_ENDPOINT | sed -E 's/(:80|:443)$//')
|
||||
export NETBIRD_MGMT_GRPC_API_ENDPOINT=${NETBIRD_MGMT_GRPC_API_ENDPOINT}
|
||||
export NETBIRD_HOTJAR_TRACK_ID=${NETBIRD_HOTJAR_TRACK_ID}
|
||||
export NETBIRD_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken}
|
||||
|
||||
REPO="https://github.com/netbirdio/netbird/"
|
||||
# this command will fetch the latest release e.g. v0.6.3
|
||||
export NETBIRD_LATEST_VERSION=$(basename $(curl -fs -o/dev/null -w %{redirect_url} ${REPO}releases/latest))
|
||||
echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"
|
||||
|
||||
# replace ENVs in the config
|
||||
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_LATEST_VERSION \$\$NETBIRD_HOTJAR_TRACK_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI"
|
||||
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_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"
|
||||
|
||||
1177
package-lock.json
generated
1177
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,13 @@
|
||||
"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",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="robots" content="noindex">
|
||||
<meta
|
||||
name="description"
|
||||
content="NetBird Management Dashboard"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
Disallow: /
|
||||
|
||||
107
src/App.tsx
107
src/App.tsx
@@ -4,7 +4,7 @@ import {apiClient, store} from "./store";
|
||||
import {hotjar} from 'react-hotjar';
|
||||
import {getConfig} from "./config";
|
||||
import Banner from "./components/Banner";
|
||||
import {Col, Layout, Row} from "antd";
|
||||
import {Col, ConfigProvider, Layout, Row} from "antd";
|
||||
import {Container} from "./components/Container";
|
||||
import Navbar from "./components/Navbar";
|
||||
import {Redirect, Route, Switch} from "react-router-dom";
|
||||
@@ -16,11 +16,12 @@ import SetupKeys from "./views/SetupKeys";
|
||||
import AccessControl from "./views/AccessControl";
|
||||
import Users from "./views/Users";
|
||||
import FooterComponent from "./components/FooterComponent";
|
||||
import {useGetAccessTokenSilently} from "./utils/token";
|
||||
import {useGetTokenSilently, useTokenSource} from "./utils/token";
|
||||
import {User} from "./store/user/types";
|
||||
import {SecureLoading} from "./components/Loading";
|
||||
import DNS from "./views/DNS";
|
||||
|
||||
import Activity from "./views/Activity";
|
||||
import Settings from "./views/Settings";
|
||||
|
||||
|
||||
const {Header, Content} = Layout;
|
||||
@@ -28,8 +29,9 @@ const {Header, Content} = Layout;
|
||||
function App() {
|
||||
const run = useRef(false)
|
||||
const [show, setShow] = useState(false)
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently();
|
||||
const {hotjarTrackID} = getConfig();
|
||||
const {hotjarTrackID,tokenSource} = getConfig();
|
||||
useTokenSource(tokenSource)
|
||||
const {getTokenSilently} = useGetTokenSilently();
|
||||
// @ts-ignore
|
||||
if (hotjarTrackID && window._DATADOG_SYNTHETICS_BROWSER === undefined) {
|
||||
hotjar.initialize(hotjarTrackID, 6);
|
||||
@@ -54,9 +56,9 @@ function App() {
|
||||
useEffect(() => {
|
||||
if (!run.current) {
|
||||
run.current = true
|
||||
apiClient.request<User[]>('GET', `/api/users`, {getAccessTokenSilently: getAccessTokenSilently})
|
||||
apiClient.request<User[]>('GET', `/api/users`, {getAccessTokenSilently: getTokenSilently})
|
||||
.then(() => {
|
||||
setShow(true)
|
||||
setShow(true)
|
||||
})
|
||||
.catch(e => {
|
||||
setShow(true)
|
||||
@@ -64,53 +66,66 @@ function App() {
|
||||
})
|
||||
}
|
||||
|
||||
}, [getAccessTokenSilently])
|
||||
}, [getTokenSilently])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
borderRadius: 4,
|
||||
colorPrimary: "#1890ff",
|
||||
fontFamily: "Arial"
|
||||
},
|
||||
components: {Badge: {fontSizeSM: 20}},
|
||||
}}
|
||||
>
|
||||
<Provider store={store}>
|
||||
{!show && <SecureLoading padding="3em" width={50} height={50}/>}
|
||||
{show &&
|
||||
<Layout>
|
||||
<Banner/>
|
||||
<Header className="header" style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-around",
|
||||
alignContent: "center"
|
||||
}}>
|
||||
<Row justify="space-around" align="middle">
|
||||
<Col span={24}>
|
||||
<Container>
|
||||
<Navbar/>
|
||||
</Container>
|
||||
</Col>
|
||||
</Row>
|
||||
</Header>
|
||||
<Content style={{minHeight: "100vh"}}>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
render={() => {
|
||||
return (
|
||||
<Redirect to="/peers"/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Route path='/peers' exact component={withOidcSecure(Peers)}/>
|
||||
<Route path="/add-peer" component={withOidcSecure(AddPeer)}/>
|
||||
<Route path="/setup-keys" component={withOidcSecure(SetupKeys)}/>
|
||||
<Route path="/acls" component={withOidcSecure(AccessControl)}/>
|
||||
<Route path="/routes" component={withOidcSecure(Routes)}/>
|
||||
<Route path="/users" component={withOidcSecure(Users)}/>
|
||||
<Route path="/dns" component={withOidcSecure(DNS)}/>
|
||||
</Switch>
|
||||
</Content>
|
||||
<FooterComponent/>
|
||||
</Layout>
|
||||
<Layout>
|
||||
<Banner/>
|
||||
<Header className="header" style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-around",
|
||||
alignContent: "center"
|
||||
}}>
|
||||
<Row justify="space-around" align="middle">
|
||||
<Col span={24}>
|
||||
<Container>
|
||||
<Navbar/>
|
||||
</Container>
|
||||
</Col>
|
||||
</Row>
|
||||
</Header>
|
||||
<Content style={{minHeight: "100vh"}}>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
render={() => {
|
||||
return (
|
||||
<Redirect to="/peers"/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Route path='/peers' exact component={withOidcSecure(Peers)}/>
|
||||
<Route path="/setup-keys" component={withOidcSecure(SetupKeys)}/>
|
||||
<Route path="/acls" component={withOidcSecure(AccessControl)}/>
|
||||
<Route path="/routes" component={withOidcSecure(Routes)}/>
|
||||
<Route path="/users" component={withOidcSecure(Users)}/>
|
||||
<Route path="/dns" component={withOidcSecure(DNS)}/>
|
||||
<Route path="/activity" component={withOidcSecure(Activity)}/>
|
||||
<Route path="/settings" component={withOidcSecure(Settings)}/>
|
||||
</Switch>
|
||||
</Content>
|
||||
<FooterComponent/>
|
||||
</Layout>
|
||||
}
|
||||
</Provider>
|
||||
|
||||
</ConfigProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
BIN
src/assets/google-play-badge.png
Normal file
BIN
src/assets/google-play-badge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
@@ -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}
|
||||
|
||||
@@ -23,7 +23,7 @@ import {Rule, RuleToSave} from "../store/rule/types";
|
||||
import {uniq} from "lodash"
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
const {Option} = Select;
|
||||
@@ -34,7 +34,7 @@ interface FormRule extends Rule {
|
||||
}
|
||||
|
||||
const AccessControlNew = () => {
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const setupNewRuleVisible = useSelector((state: RootState) => state.rule.setupNewRuleVisible)
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
@@ -103,7 +103,7 @@ const AccessControlNew = () => {
|
||||
.then((values) => {
|
||||
const ruleToSave = createRuleToSave()
|
||||
dispatch(ruleActions.saveRule.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: ruleToSave
|
||||
}))
|
||||
})
|
||||
@@ -408,9 +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/how-to/manage-network-access">Learn more about access controls</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
@@ -22,7 +22,7 @@ const Banner = () => {
|
||||
const linkLearnMore = () => {
|
||||
return (
|
||||
<a
|
||||
href="https://netbird.io/docs/how-to-guides/nameservers"
|
||||
href="https://docs.netbird.io/how-to/manage-dns-in-your-network"
|
||||
className="font-bold underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
@@ -36,7 +36,7 @@ const Banner = () => {
|
||||
|
||||
if((!stored_banner_closed || stored_banner_closed !== 'true') ||
|
||||
(!store_banner_md5 || store_banner_md5 !== announcement_md5)) {
|
||||
setShow(true);
|
||||
//setShow(true);
|
||||
localStorage.setItem(banner_md5_key,announcement_md5);
|
||||
localStorage.setItem(banner_closed_key,'false');
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ import {RuleObject} from "antd/lib/form";
|
||||
import cidrRegex from 'cidr-regex';
|
||||
import {NameServer, NameServerGroup, NameServerGroupToSave} from "../store/nameservers/types";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups"
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
|
||||
@@ -51,7 +51,7 @@ const NameServerGroupUpdate = () => {
|
||||
selectValidator
|
||||
} = useGetGroupTagHelpers()
|
||||
const dispatch = useDispatch()
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const {Option} = Select;
|
||||
const nsGroup = useSelector((state: RootState) => state.nameserverGroup.nameserverGroup)
|
||||
const setupNewNameServerGroupVisible = useSelector((state: RootState) => state.nameserverGroup.setupNewNameServerGroupVisible)
|
||||
@@ -220,7 +220,7 @@ const NameServerGroupUpdate = () => {
|
||||
.then((values) => {
|
||||
const nsGroupToSave = createNSGroupToSave(values as NameServerGroup)
|
||||
dispatch(nsGroupActions.saveNameServerGroup.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: nsGroupToSave
|
||||
}))
|
||||
|
||||
@@ -604,9 +604,7 @@ const NameServerGroupUpdate = () => {
|
||||
<Col span={24}>
|
||||
<Divider></Divider>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
|
||||
href="https://netbird.io/docs/how-to-guides/nameservers"
|
||||
style={{color: 'rgb(07, 114, 128)'}}>Learn
|
||||
more about nameservers</Button>
|
||||
href="https://docs.netbird.io/how-to/manage-dns-in-your-network">Learn more about nameservers</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>) :
|
||||
|
||||
@@ -5,13 +5,14 @@ import {Avatar, Button, Col, Dropdown, Grid, Menu, Row} from 'antd'
|
||||
import {ItemType} from "antd/lib/menu/hooks/useItems";
|
||||
import {AvatarSize} from "antd/es/avatar/SizeContext";
|
||||
import {UserOutlined} from '@ant-design/icons';
|
||||
import {useOidc, useOidcUser} from '@axa-fr/react-oidc';
|
||||
import {useOidc, useOidcIdToken, useOidcUser} from '@axa-fr/react-oidc';
|
||||
import {getConfig} from "../config";
|
||||
import {User} from "../store/user/types";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as userActions} from "../store/user";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {actions as personalAccessTokenActions} from "../store/personal-access-token";
|
||||
|
||||
const {useBreakpoint} = Grid;
|
||||
|
||||
@@ -19,10 +20,11 @@ const Navbar = () => {
|
||||
let location = useLocation();
|
||||
const config = getConfig();
|
||||
const { logout } = useOidc();
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const {oidcUser} = useOidcUser();
|
||||
const {idTokenPayload} = useOidcIdToken()
|
||||
const user = oidcUser;
|
||||
const [currentUser, setCurrentUser] = useState({} as User)
|
||||
|
||||
@@ -34,22 +36,37 @@ const Navbar = () => {
|
||||
|
||||
const items = [
|
||||
{label: (<Link to="/peers">Peers</Link>), key: '/peers'},
|
||||
{label: (<Link to="/add-peer">Add Peer</Link>), key: '/add-peer'},
|
||||
{label: (<Link to="/setup-keys">Setup Keys</Link>), key: '/setup-keys'},
|
||||
{label: (<Link to="/acls">Access Control</Link>), key: '/acls'},
|
||||
{label: (<Link to="/routes">Network Routes</Link>), key: '/routes'},
|
||||
{ label: (<Link to="/dns">DNS</Link>), key: '/dns' },
|
||||
{label: (<Link to="/users">Users</Link>), key: '/users'}
|
||||
{label: (<Link to="/users">Users</Link>), key: '/users'},
|
||||
{label: (<Link to="/activity">Activity</Link>), key: '/activity'},
|
||||
{label: (<Link to="/settings">Settings</Link>), key: '/settings'}
|
||||
] as ItemType[]
|
||||
|
||||
const userEmailKey = 'user-email'
|
||||
const userLogoutKey = 'user-logout'
|
||||
const userDividerKey = 'user-divider'
|
||||
const adminOnlyTabs = ["/setup-keys", "/acls", "/routes", "/dns"]
|
||||
const adminOnlyTabs = ["/setup-keys", "/acls", "/routes", "/dns", "/activity", "/settings"]
|
||||
const [menuItems, setMenuItems] = useState(items)
|
||||
const logoutWithRedirect = () =>
|
||||
logout("/", {client_id: config.clientId});
|
||||
|
||||
const openPersonalUserPage = () => {
|
||||
dispatch(userActions.setUser({
|
||||
id: currentUser.id,
|
||||
email: currentUser.email,
|
||||
role: currentUser.role,
|
||||
auto_groups: currentUser.auto_groups ? currentUser.auto_groups : [],
|
||||
name: currentUser.name,
|
||||
is_current: currentUser.is_current,
|
||||
is_service_user: currentUser.is_service_user,
|
||||
} as User));
|
||||
dispatch(userActions.setUserTabOpen("Users"));
|
||||
dispatch(userActions.setEditUserPopupVisible(true));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const fs = items.filter(m => showTab(m?.key?.toString(), currentUser) && m?.key !== userEmailKey && m?.key !== userLogoutKey && m?.key !== userDividerKey)
|
||||
if (screens.xs === true) {
|
||||
@@ -78,15 +95,19 @@ const Navbar = () => {
|
||||
window.location.pathname !== '/peers' &&
|
||||
window.location.pathname !== '/users') {
|
||||
setIsRefreshingUserState(true)
|
||||
dispatch(userActions.getUsers.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}))
|
||||
dispatch(userActions.getUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}))
|
||||
return
|
||||
}
|
||||
if (users.length === 0 && isRefreshingUserState) {
|
||||
return
|
||||
}
|
||||
let runUser = oidcUser
|
||||
if (!oidcUser) {
|
||||
runUser = idTokenPayload
|
||||
}
|
||||
setIsRefreshingUserState(false)
|
||||
if (oidcUser && oidcUser.sub) {
|
||||
const found = users.find(u => u.id == oidcUser.sub)
|
||||
if (runUser) {
|
||||
const found = users.find(u => u.is_current ? u.is_current : runUser.sub ? u.id == runUser.sub : false)
|
||||
if (found) {
|
||||
setCurrentUser(found)
|
||||
}
|
||||
@@ -108,7 +129,7 @@ const Navbar = () => {
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
label: <>{user?.email}</>,
|
||||
label: (<Link to="/users" onClick={openPersonalUserPage}>{user?.email}</Link>),
|
||||
key: '0',
|
||||
},
|
||||
{
|
||||
@@ -142,6 +163,10 @@ const Navbar = () => {
|
||||
<Col flex="1 1 auto">
|
||||
<div>
|
||||
<Menu mode="horizontal" selectable={true} selectedKeys={[location.pathname]}
|
||||
onSelect={(e) => {
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null))
|
||||
}}
|
||||
defaultSelectedKeys={[location.pathname]} items={menuItems}/>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
@@ -2,14 +2,14 @@ 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, Collapse, 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 {FormPeer, Peer, PeerGroupsToSave} from "../store/peer/types";
|
||||
import {Group, GroupPeer} from "../store/group/types";
|
||||
import {CloseOutlined, EditOutlined} from "@ant-design/icons";
|
||||
import {RuleObject} from 'antd/lib/form';
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {timeAgo} from "../utils/common";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
@@ -18,7 +18,8 @@ const {Panel} = Collapse;
|
||||
const punycode = require('punycode/')
|
||||
|
||||
const PeerUpdate = () => {
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const users = useSelector((state: RootState) => state.user.data)
|
||||
@@ -190,6 +191,7 @@ const PeerUpdate = () => {
|
||||
setCallingPeerAPI(false)
|
||||
setCallingPeerAPI(false)
|
||||
setSubmitRunning(false)
|
||||
setEstimatedName("")
|
||||
}
|
||||
|
||||
const noUpdateToGroups = (): Boolean => {
|
||||
@@ -200,6 +202,10 @@ const PeerUpdate = () => {
|
||||
return !formPeer.name || formPeer.name === peer.name
|
||||
}
|
||||
|
||||
const noUpdateToLoginExpiration = (): Boolean => {
|
||||
return formPeer.login_expiration_enabled === peer.login_expiration_enabled
|
||||
}
|
||||
|
||||
const onChange = (data: any) => {
|
||||
setFormPeer({...formPeer, ...data})
|
||||
}
|
||||
@@ -232,7 +238,8 @@ const PeerUpdate = () => {
|
||||
return {
|
||||
id: formPeer.id,
|
||||
ssh_enabled: formPeer.ssh_enabled,
|
||||
name: formPeer.name
|
||||
name: formPeer.name,
|
||||
login_expiration_enabled: formPeer.login_expiration_enabled
|
||||
} as Peer
|
||||
}
|
||||
|
||||
@@ -240,18 +247,18 @@ const PeerUpdate = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
setSubmitRunning(true)
|
||||
if (!noUpdateToName()) {
|
||||
if (!noUpdateToName() || !noUpdateToLoginExpiration()) {
|
||||
const peerUpdate = createPeerToSave()
|
||||
setCallingPeerAPI(true)
|
||||
dispatch(peerActions.updatePeer.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: peerUpdate
|
||||
}))
|
||||
}
|
||||
if (peerGroupsToSave.groupsToRemove.length || peerGroupsToSave.groupsToAdd.length || peerGroupsToSave.groupsNoId.length) {
|
||||
if (!noUpdateToGroups()) {
|
||||
setCallingGroupAPI(true)
|
||||
dispatch(peerActions.saveGroups.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: peerGroupsToSave
|
||||
}))
|
||||
}
|
||||
@@ -303,7 +310,7 @@ const PeerUpdate = () => {
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button onClick={onCancel} disabled={savedGroups.loading}>Cancel</Button>
|
||||
<Button type="primary"
|
||||
disabled={(savedGroups.loading || updatedPeers.loading) || (noUpdateToGroups() && noUpdateToName())}
|
||||
disabled={(savedGroups.loading || updatedPeers.loading) || (noUpdateToGroups() && noUpdateToName() && noUpdateToLoginExpiration())}
|
||||
onClick={handleFormSubmit}>Save</Button>
|
||||
</Space>
|
||||
}
|
||||
@@ -353,7 +360,7 @@ const PeerUpdate = () => {
|
||||
max={59}/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Possible domain name after saving"
|
||||
label="New peer domain name preview"
|
||||
tooltip="If the domain name already exists, we add an increment number suffix to it"
|
||||
style={{margin: '1px'}}
|
||||
>
|
||||
@@ -435,6 +442,20 @@ const PeerUpdate = () => {
|
||||
</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"
|
||||
@@ -458,23 +479,6 @@ const PeerUpdate = () => {
|
||||
</Select>
|
||||
</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>
|
||||
</Col>*/}
|
||||
{/*<Col span={24}>
|
||||
<Divider orientation="left" plain style={{color: "#5a5c5a"}}>
|
||||
System Info
|
||||
</Divider>
|
||||
</Col>*/}
|
||||
<Col span={24}>
|
||||
<Collapse onChange={onChange} bordered={false} ghost={true}
|
||||
style={{color: "#5a5c5a"}}>
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
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 cidrRegex from 'cidr-regex';
|
||||
@@ -30,7 +30,8 @@ import {
|
||||
routePeerSeparator,
|
||||
transformGroupedDataTable
|
||||
} from '../utils/routes'
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
|
||||
@@ -38,7 +39,18 @@ interface FormRoute extends Route {
|
||||
}
|
||||
|
||||
const RouteUpdate = () => {
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
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)
|
||||
@@ -46,7 +58,6 @@ const RouteUpdate = () => {
|
||||
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)
|
||||
@@ -62,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 {
|
||||
@@ -77,7 +89,7 @@ const RouteUpdate = () => {
|
||||
setStatusMSG(defaultStatusMSG)
|
||||
setPreviousRouteKey("")
|
||||
}
|
||||
}, [setupNewRouteHA])
|
||||
}, [newRoute])
|
||||
|
||||
useEffect(() => {
|
||||
if (editName) inputNameRef.current!.focus({
|
||||
@@ -96,16 +108,23 @@ const RouteUpdate = () => {
|
||||
|
||||
const fRoute = {
|
||||
...route,
|
||||
groups: getGroupNamesFromIDs(route.groups)
|
||||
} as FormRoute
|
||||
setFormRoute(fRoute)
|
||||
setPreviousRouteKey(fRoute.network_id + fRoute.network)
|
||||
if (!route.network_id) {
|
||||
setNewRoute(true)
|
||||
} else {
|
||||
setNewRoute(false)
|
||||
}
|
||||
form.setFieldsValue(fRoute)
|
||||
}, [route])
|
||||
|
||||
peers.forEach((p) => {
|
||||
let os: string
|
||||
os = p.os
|
||||
if (!os.toLowerCase().startsWith("darwin") && !os.toLowerCase().startsWith("windows")) {
|
||||
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),
|
||||
@@ -114,15 +133,21 @@ const RouteUpdate = () => {
|
||||
}
|
||||
})
|
||||
|
||||
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]
|
||||
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,
|
||||
@@ -131,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 = () => {
|
||||
@@ -141,11 +168,11 @@ const RouteUpdate = () => {
|
||||
if (!setupNewRouteHA || formRoute.peer != '') {
|
||||
const routeToSave = createRouteToSave(formRoute)
|
||||
dispatch(routeActions.saveRoute.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
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) => {
|
||||
@@ -158,7 +185,7 @@ const RouteUpdate = () => {
|
||||
}
|
||||
const routeToSave = createRouteToSave(updateRoute)
|
||||
dispatch(routeActions.saveRoute.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: routeToSave
|
||||
}))
|
||||
})
|
||||
@@ -195,13 +222,14 @@ const RouteUpdate = () => {
|
||||
setVisibleNewRoute(false)
|
||||
setSetupNewRouteHA(false)
|
||||
setPreviousRouteKey("")
|
||||
setNewRoute(false)
|
||||
}
|
||||
|
||||
const onChange = (data: any) => {
|
||||
setFormRoute({...formRoute, ...data})
|
||||
}
|
||||
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
const peerDropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
</>
|
||||
@@ -227,13 +255,32 @@ 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}
|
||||
@@ -241,11 +288,11 @@ const RouteUpdate = () => {
|
||||
<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>
|
||||
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"}}>
|
||||
@@ -278,7 +325,7 @@ const RouteUpdate = () => {
|
||||
}]}
|
||||
>
|
||||
<Input placeholder="e.g. aws-eu-central-1-vpc" ref={inputNameRef}
|
||||
disabled={!setupNewRouteHA}
|
||||
disabled={!setupNewRouteHA && !newRoute}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)} autoComplete="off"
|
||||
maxLength={40}/>
|
||||
@@ -294,7 +341,7 @@ const RouteUpdate = () => {
|
||||
style={{marginTop: 24}}
|
||||
>
|
||||
<Input placeholder="Add description..." ref={inputDescriptionRef}
|
||||
disabled={!setupNewRouteHA}
|
||||
disabled={!setupNewRouteHA && !newRoute}
|
||||
onPressEnter={() => toggleEditDescription(false)}
|
||||
onBlur={() => toggleEditDescription(false)}
|
||||
autoComplete="off" maxLength={200}/>
|
||||
@@ -321,7 +368,7 @@ 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}
|
||||
<Input placeholder="e.g. 172.16.0.0/16" disabled={!setupNewRouteHA && !newRoute}
|
||||
autoComplete="off" minLength={9} maxLength={43}/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
@@ -343,12 +390,13 @@ 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}
|
||||
dropdownRender={peerDropDownRender}
|
||||
options={options}
|
||||
allowClear={true}
|
||||
/>
|
||||
@@ -360,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}>
|
||||
@@ -372,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">
|
||||
@@ -388,9 +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://docs.netbird.io/how-to/routing-traffic-to-private-networks">Learn more about network routes</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
@@ -1,111 +1,96 @@
|
||||
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,
|
||||
InputNumber,
|
||||
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 {FormSetupKey, SetupKey, SetupKeyToSave} from "../store/setup-key/types";
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
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 {useGetAccessTokenSilently} from "../utils/token";
|
||||
import ExpiresInInput, {ExpiresInValue} from "../views/ExpiresInInput";
|
||||
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 ExpiresInDefault: ExpiresInValue = {number: 30, interval: "Days"}
|
||||
const customExpiresFormat = (value: Date): string | null => {
|
||||
return formatDate(value);
|
||||
};
|
||||
|
||||
const customExpiresFormat: DatePickerProps['format'] = value => {
|
||||
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
|
||||
}
|
||||
|
||||
let ago = timeAgo(value.toString());
|
||||
if (!ago) return "unused";
|
||||
|
||||
return ago;
|
||||
};
|
||||
|
||||
const SetupKeyNew = () => {
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
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 : [],
|
||||
expiresInFormatted: ExpiresInDefault
|
||||
} 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));
|
||||
|
||||
let expiresIn = expiresInToSeconds(formSetupKey.expiresInFormatted)
|
||||
const expiresIn = expiresInToSeconds(formSetupKey.expiresInFormatted);
|
||||
return {
|
||||
id: formSetupKey.id,
|
||||
name: formSetupKey.name,
|
||||
@@ -114,97 +99,79 @@ const SetupKeyNew = () => {
|
||||
revoked: formSetupKey.revoked,
|
||||
groupsToCreate: groupsToCreate,
|
||||
expires_in: expiresIn,
|
||||
usage_limit: formSetupKey.usage_limit
|
||||
} as SetupKeyToSave
|
||||
}
|
||||
const expiresInToSeconds = (expiresIn: ExpiresInValue): number => {
|
||||
if (!expiresIn.number || !expiresIn.interval) {
|
||||
return 0
|
||||
}
|
||||
let multiplier = 0
|
||||
switch (expiresIn.interval.toLowerCase()) {
|
||||
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
|
||||
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);
|
||||
}
|
||||
|
||||
return expiresIn.number * multiplier
|
||||
|
||||
}
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
let setupKeyToSave = createSetupKeyToSave()
|
||||
dispatch(setupKeyActions.saveSetupKey.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: setupKeyToSave
|
||||
}))
|
||||
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: "one-off",
|
||||
key: "",
|
||||
last_used: "",
|
||||
expires: "",
|
||||
state: "valid",
|
||||
auto_groups: new Array(),
|
||||
usage_limit: 0,
|
||||
used_times: 0,
|
||||
expires_in: 0
|
||||
} as SetupKey))
|
||||
setFormSetupKey({} as FormSetupKey)
|
||||
setVisibleNewSetupKey(false)
|
||||
}
|
||||
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();
|
||||
@@ -221,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>
|
||||
@@ -252,261 +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 inputLabel = (text: any) => (
|
||||
<>
|
||||
<span>{text}</span>
|
||||
<Tag color="red">{formSetupKey.state}</Tag>
|
||||
</>
|
||||
)
|
||||
);
|
||||
|
||||
const changesDetected = (): boolean => {
|
||||
return formSetupKey.name == null || formSetupKey.name !== setupKey.name || groupsChanged()
|
||||
|| formSetupKey.usage_limit !== setupKey.usage_limit
|
||||
}
|
||||
return (
|
||||
formSetupKey.name == null ||
|
||||
formSetupKey.name !== setupKey.name ||
|
||||
groupsChanged() ||
|
||||
formSetupKey.usage_limit !== setupKey.usage_limit
|
||||
);
|
||||
};
|
||||
|
||||
const groupsChanged = (): boolean => {
|
||||
if (formSetupKey.autoGroupNames.length != setupKey.auto_groups.length) {
|
||||
return true
|
||||
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 || '') || []
|
||||
const formGroupIds =
|
||||
groups?.filter((g) => formSetupKey.autoGroupNames.includes(g.name)).map((g) => g.id || "") || [];
|
||||
|
||||
return setupKey.auto_groups?.filter(g => !formGroupIds.includes(g)).length > 0
|
||||
}
|
||||
|
||||
const checkExpiresIn = (_: any, value: { number: number }) => {
|
||||
if (value.number > 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error("Expiration must be greater than zero"));
|
||||
return setupKey.auto_groups?.filter((g) => !formGroupIds.includes(g)).length > 0;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{setupKey &&
|
||||
<Drawer
|
||||
forceRender={true}
|
||||
headerStyle={{display: "none"}}
|
||||
open={setupNewKeyVisible}
|
||||
bodyStyle={{paddingBottom: 80}}
|
||||
onClose={onCancel}
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button disabled={savedSetupKey.loading} onClick={onCancel}>Cancel</Button>
|
||||
<Button type="primary" disabled={savedSetupKey.loading || !changesDetected()}
|
||||
onClick={handleFormSubmit}>{`${formSetupKey.id ? 'Save' : 'Create'}`}</Button>
|
||||
</Space>
|
||||
}
|
||||
<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}
|
||||
>
|
||||
<Form layout="vertical" hideRequiredMark form={form} onValuesChange={onChange}
|
||||
initialValues={{
|
||||
expiresInFormatted: ExpiresInDefault,
|
||||
}}
|
||||
<Button onClick={onCancel}>Cancel</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
style={{
|
||||
height: "100%",
|
||||
fontSize: "14px",
|
||||
borderRadius: "2px",
|
||||
}}
|
||||
disabled={savedSetupKey.loading || !changesDetected()}
|
||||
onClick={handleFormSubmit}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
{`${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,
|
||||
}}
|
||||
>
|
||||
|
||||
<Row>
|
||||
{isEditMode ? (
|
||||
<></>
|
||||
) : (
|
||||
<Col span={24}>
|
||||
<Header style={{margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px"}}>
|
||||
<Row align="top">
|
||||
<Col flex="none" style={{display: "flex"}}>
|
||||
{!editName && setupKey.id &&
|
||||
<button type="button" aria-label="Close" className="ant-drawer-close"
|
||||
style={{paddingTop: 3}}
|
||||
onClick={onCancel}>
|
||||
<span role="img" aria-label="close"
|
||||
className="anticon anticon-close">
|
||||
<CloseOutlined size={16}/>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
{!editName && setupKey.id && formSetupKey.name ? (
|
||||
<div className={"access-control input-text ant-drawer-title"}
|
||||
onClick={() => toggleEditName(true)}>{formSetupKey.name ? formSetupKey.name : setupKey.name}
|
||||
<EditOutlined/></div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Name"
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please add a new name for this peer',
|
||||
whitespace: true
|
||||
}]}
|
||||
>
|
||||
<Input
|
||||
placeholder={setupKey.name}
|
||||
ref={inputNameRef}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Header>
|
||||
<Paragraph style={{fontWeight: "bold"}}>
|
||||
Name
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>
|
||||
Set an easily identifiable name for your key
|
||||
</Paragraph>
|
||||
</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}
|
||||
style={{color: "#5a5c5a"}}
|
||||
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%", color: "#5a5c5a"}}
|
||||
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%", color: "#5a5c5a"}}
|
||||
format={customLastUsedFormat}/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
}
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="type"
|
||||
label="Type"
|
||||
rules={[{required: true, message: 'Please enter key type'}]}
|
||||
>
|
||||
<Radio.Group style={{display: 'flex'}} disabled={setupKey.id}>
|
||||
<Space direction="vertical" style={{flex: 1}}>
|
||||
<List
|
||||
size="large"
|
||||
bordered
|
||||
>
|
||||
<List.Item>
|
||||
<Radio value={"one-off"}>
|
||||
<Space direction="vertical" size="small">
|
||||
<Text strong>One-off</Text>
|
||||
<Text>This key can be used only once</Text>
|
||||
</Space>
|
||||
</Radio>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<Radio value={"reusable"}>
|
||||
<Space direction="vertical" size="small">
|
||||
<Text strong>Reusable</Text>
|
||||
<Text>This type of a setup key allows to enroll multiple
|
||||
machines</Text>
|
||||
</Space>
|
||||
</Radio>
|
||||
</List.Item>
|
||||
</List>
|
||||
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{!setupKey.id &&
|
||||
<Col span={24}>
|
||||
<Form.Item name="expiresInFormatted" label="Expires In"
|
||||
rules={[{validator: checkExpiresIn}]}>
|
||||
<ExpiresInInput/>
|
||||
</Form.Item>
|
||||
</Col>}
|
||||
<Col span={12}>
|
||||
<Form.Item name="usage_limit"
|
||||
label="Usage Limit"
|
||||
tooltip="Limit the number of times this key can be used. Use 0 for unlimited use."
|
||||
>
|
||||
<InputNumber min={0} defaultValue={0} disabled={setupKey.id || formSetupKey.type !== "reusable"}
|
||||
style={{width: "100%"}}
|
||||
<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)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="used_times"
|
||||
label="Used Times"
|
||||
>
|
||||
<InputNumber min={0} defaultValue={0} disabled={true}
|
||||
style={{width: "100%"}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
) : (
|
||||
<Input placeholder={`e.g. "AWS servers"`}/>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{isEditMode && (
|
||||
<Row style={{marginTop: "20px"}}>
|
||||
<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}]}
|
||||
<Paragraph
|
||||
style={{whiteSpace: "pre-line", fontWeight: "bold", margin: 0}}
|
||||
>
|
||||
<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://docs.netbird.io/how-to/register-machines-using-setup-keys">
|
||||
{" "}
|
||||
setup keys
|
||||
</a>
|
||||
</Text>
|
||||
</Row>
|
||||
</Form>
|
||||
</Container>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
;
|
||||
};
|
||||
|
||||
export default SetupKeyNew
|
||||
export default SetupKeyNew;
|
||||
535
src/components/UserEdit.tsx
Normal file
535
src/components/UserEdit.tsx
Normal file
@@ -0,0 +1,535 @@
|
||||
import {
|
||||
Badge,
|
||||
Breadcrumb,
|
||||
Button, Card,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
List, Modal,
|
||||
Row,
|
||||
Select,
|
||||
Skeleton,
|
||||
Space, Switch, Table,
|
||||
Tag, Typography
|
||||
} from "antd";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as userActions} from "../store/user";
|
||||
import {FormUser, User, UserToSave} from "../store/user/types";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import {CustomTagProps} from "rc-select/lib/BaseSelect";
|
||||
import {actions as groupActions} from "../store/group";
|
||||
import {actions as personalAccessTokenActions} from "../store/personal-access-token";
|
||||
import {PersonalAccessToken, PersonalAccessTokenCreate, SpecificPAT} from "../store/personal-access-token/types";
|
||||
import tableSpin from "./Spin";
|
||||
import AddPATPopup from "./popups/AddPATPopup";
|
||||
import {fullDate} from "../utils/common";
|
||||
import {ExclamationCircleOutlined} from "@ant-design/icons";
|
||||
import {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 [isAdmin, setIsAdmin] = useState(false);
|
||||
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedUser.loading) return
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null))
|
||||
setFormUser({} as FormUser)
|
||||
dispatch(userActions.setEditUserPopupVisible(false));
|
||||
}
|
||||
|
||||
const createUserToSave = (values: any): UserToSave => {
|
||||
const autoGroups = groups?.filter(g => values.autoGroupsNames.includes(g.name)).map(g => g.id || '') || []
|
||||
// find groups that do not yet exist (newly added by the user)
|
||||
const allGroupsNames: string[] = groups?.map(g => g.name);
|
||||
const groupsToCreate = values.autoGroupsNames.filter((s: string) => !allGroupsNames.includes(s))
|
||||
let userID = user ? user.id : ''
|
||||
let isServiceUser = user ? user.is_service_user : false
|
||||
return {
|
||||
id: userID,
|
||||
role: values.role,
|
||||
name: values.name,
|
||||
groupsToCreate: groupsToCreate,
|
||||
auto_groups: autoGroups,
|
||||
is_service_user: isServiceUser,
|
||||
is_blocked: values.is_blocked
|
||||
} as UserToSave
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if(users) {
|
||||
let currentUser = users.find((user) => user.is_current)
|
||||
if(currentUser) {
|
||||
setIsAdmin(currentUser.role === "admin");
|
||||
}
|
||||
}
|
||||
}, [users])
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
let userToSave = createUserToSave(values)
|
||||
dispatch(userActions.saveUser.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: userToSave
|
||||
}))
|
||||
dispatch(userActions.setEditUserPopupVisible(false));
|
||||
dispatch(userActions.setUser(null as unknown as User))
|
||||
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null))
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log('errorInfo', errorInfo)
|
||||
});
|
||||
};
|
||||
|
||||
const onClickAddNewPersonalAccessToken = () => {
|
||||
dispatch(personalAccessTokenActions.setPersonalAccessToken({
|
||||
user_id: "",
|
||||
name: "",
|
||||
expires_in: 7
|
||||
} as PersonalAccessTokenCreate))
|
||||
dispatch(personalAccessTokenActions.setNewPersonalAccessTokenPopupVisible(true));
|
||||
}
|
||||
|
||||
const onBreadcrumbUsersClick = (key: string) => {
|
||||
if (savedUser.loading) return
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null))
|
||||
dispatch(userActions.setUserTabOpen(key))
|
||||
}
|
||||
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = []
|
||||
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v)
|
||||
}
|
||||
})
|
||||
|
||||
if (hasSpaceNamed.length) {
|
||||
return Promise.reject(new Error("Group names with just spaces are not allowed"))
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const 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,
|
||||
is_blocked: user.is_blocked,
|
||||
autoGroupsNames: currentGroups,
|
||||
})
|
||||
}
|
||||
}, [form, user, currentGroups])
|
||||
|
||||
|
||||
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 onClick={() => onBreadcrumbUsersClick("Users")}>All Users</a>,
|
||||
},
|
||||
{
|
||||
title: <a 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,
|
||||
is_blocked: formUser.is_blocked,
|
||||
autoGroupsNames: formUser.autoGroupsNames,
|
||||
}}
|
||||
>
|
||||
<Row style={{paddingBottom: "15px"}}>
|
||||
{!user.is_service_user &&
|
||||
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
|
||||
<Form.Item
|
||||
name="email"
|
||||
label={<Text style={{}}>Email</Text>}
|
||||
style={{marginRight: "70px"}}
|
||||
>
|
||||
<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 xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
|
||||
<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}]}
|
||||
style={{marginRight: "70px"}}
|
||||
>
|
||||
<Select mode="tags"
|
||||
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>
|
||||
|
||||
{!user.is_current && isAdmin && (
|
||||
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
|
||||
<Form.Item
|
||||
valuePropName="checked"
|
||||
name="is_blocked"
|
||||
label="Block user"
|
||||
style={{marginRight: "50px"}}
|
||||
>
|
||||
<Switch/>
|
||||
</Form.Item>
|
||||
</Col>)}
|
||||
</Row>}
|
||||
|
||||
<Space style={{display: 'flex', justifyContent: 'start'}}>
|
||||
<Button disabled={loading} onClick={onCancel}>Cancel</Button>
|
||||
<Button type="primary"
|
||||
onClick={handleFormSubmit}>Save</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{user && (user.is_current || user.is_service_user) && <Card
|
||||
bordered={true}
|
||||
loading={loading}
|
||||
style={{marginBottom: "7px"}}
|
||||
>
|
||||
<div style={{maxWidth: "800px"}}>
|
||||
<Paragraph
|
||||
style={{textAlign: "left", whiteSpace: "pre-line", fontSize: "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,413 +0,0 @@
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {Alert, Button, Col, Divider, Drawer, Form, Input, Modal, Row, Select, Space, Tag, Typography} from "antd";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {CloseOutlined, EditOutlined, ExclamationCircleOutlined} from "@ant-design/icons";
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import {Group} from "../store/group/types";
|
||||
import {FormUser, User, UserToSave} from "../store/user/types";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import {CustomTagProps} from "rc-select/lib/BaseSelect";
|
||||
import {actions as userActions} from "../store/user";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useOidcUser} from "@axa-fr/react-oidc";
|
||||
|
||||
const {Paragraph, Text} = Typography;
|
||||
|
||||
const {confirm} = Modal;
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
const UserUpdate = () => {
|
||||
const {oidcUser} = useOidcUser();
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const user = useSelector((state: RootState) => state.user.user)
|
||||
const savedUser = useSelector((state: RootState) => state.user.savedUser)
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const users = useSelector((state: RootState) => state.user.data)
|
||||
const updateUserDrawerVisible = useSelector((state: RootState) => state.user.updateUserDrawerVisible)
|
||||
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[])
|
||||
const [tagGroups, setTagGroups] = useState([] as string[])
|
||||
const [editName, setEditName] = useState(false)
|
||||
const inputNameRef = useRef<any>(null)
|
||||
|
||||
const [formUser, setFormUser] = useState({} as FormUser)
|
||||
const [currentUser, setCurrentUser] = useState({} as User)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
useEffect(() => {
|
||||
if (editName) inputNameRef.current!.focus({
|
||||
cursor: 'end',
|
||||
});
|
||||
}, [editName]);
|
||||
|
||||
const toggleEditName = (status: boolean) => {
|
||||
setEditName(status);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setTagGroups(groups?.filter(g => g.name != "All").map(g => g.name) || [])
|
||||
}, [groups])
|
||||
useEffect(() => {
|
||||
if (oidcUser && oidcUser.sub) {
|
||||
const found = users.find(u => u.id == oidcUser.sub)
|
||||
if (found) {
|
||||
setCurrentUser(found)
|
||||
}
|
||||
} else {
|
||||
setCurrentUser({} as User)
|
||||
}
|
||||
|
||||
}, [oidcUser, users])
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return
|
||||
|
||||
let allGroups = new Map<string, Group>();
|
||||
groups.forEach(g => {
|
||||
allGroups.set(g.id!, g)
|
||||
})
|
||||
|
||||
if (!user.auto_groups) {
|
||||
user.auto_groups = []
|
||||
}
|
||||
let formKeyGroups = user.auto_groups.filter(g => allGroups.get(g)).map(g => allGroups.get(g)!.name)
|
||||
|
||||
const fUser = {
|
||||
...user,
|
||||
autoGroupsNames: user.auto_groups ? formKeyGroups : [],
|
||||
} as FormUser
|
||||
setFormUser(fUser)
|
||||
form.setFieldsValue(fUser)
|
||||
}, [user])
|
||||
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = []
|
||||
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v)
|
||||
}
|
||||
})
|
||||
|
||||
if (hasSpaceNamed.length) {
|
||||
return Promise.reject(new Error("Group names with just spaces are not allowed"))
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const tagRender = (props: CustomTagProps) => {
|
||||
const {label, value, closable, onClose} = props;
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<Tag
|
||||
color="blue"
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{value}</strong>
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
const optionRender = (label: string) => {
|
||||
let peersCount = ''
|
||||
const g = groups.find(_g => _g.name === label)
|
||||
if (g) peersCount = ` - ${g.peers_count || 0} ${(!g.peers_count || parseInt(g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{label}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{margin: '8px 0'}}/>
|
||||
<Row style={{padding: '0 8px 4px'}}>
|
||||
<Col flex="auto">
|
||||
<span style={{color: "#9CA3AF"}}>Add new group by pressing "Enter"</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
|
||||
fill="#9CA3AF"/>
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
|
||||
const handleChangeTags = (value: string[]) => {
|
||||
let validatedValues: string[] = []
|
||||
value.forEach(function (v) {
|
||||
if (v.trim().length) {
|
||||
validatedValues.push(v)
|
||||
}
|
||||
})
|
||||
setSelectedTagGroups(validatedValues)
|
||||
};
|
||||
|
||||
const createUserToSave = (): UserToSave => {
|
||||
const autoGroups = groups?.filter(g => formUser.autoGroupsNames.includes(g.name)).map(g => g.id || '') || []
|
||||
// find groups that do not yet exist (newly added by the user)
|
||||
const allGroupsNames: string[] = groups?.map(g => g.name);
|
||||
const groupsToCreate = formUser.autoGroupsNames.filter(s => !allGroupsNames.includes(s))
|
||||
return {
|
||||
id: formUser.id,
|
||||
email: formUser.email,
|
||||
role: formUser.role,
|
||||
name: formUser.name,
|
||||
groupsToCreate: groupsToCreate,
|
||||
auto_groups: autoGroups,
|
||||
} as UserToSave
|
||||
}
|
||||
|
||||
const showConfirmChangeRole = (userToSave: UserToSave) => {
|
||||
let content = <Paragraph>With this action, you will remove the administrative privileges of your user.
|
||||
Your user will be limited to read-only operations in this account. Are you sure?</Paragraph>
|
||||
let contentModule = <div>{content}</div>
|
||||
|
||||
let name = formUser ? formUser.email : ''
|
||||
confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
title: "Update user \"" + name + "\"",
|
||||
width: 600,
|
||||
content: contentModule,
|
||||
okType: 'danger',
|
||||
onOk() {
|
||||
dispatch(userActions.saveUser.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: userToSave
|
||||
}))
|
||||
},
|
||||
onCancel() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// check if currentUser (who is doing the modification) removes the administrative privileges from themselves
|
||||
const isShowConfirmWarning = (userToSave: UserToSave): boolean => {
|
||||
return currentUser.id == userToSave.id && currentUser.role === "admin" && userToSave.role !== "admin"
|
||||
}
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
let userToSave = createUserToSave()
|
||||
if (isShowConfirmWarning(userToSave)) {
|
||||
showConfirmChangeRole(userToSave)
|
||||
} else {
|
||||
dispatch(userActions.saveUser.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: userToSave
|
||||
}))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log('errorInfo', errorInfo)
|
||||
});
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedUser.loading) return
|
||||
dispatch(userActions.setUser({
|
||||
id: "",
|
||||
email: "",
|
||||
role: "",
|
||||
status: "",
|
||||
auto_groups: [],
|
||||
name: user.name
|
||||
} as User));
|
||||
setFormUser({} as FormUser)
|
||||
toggleEditName(false)
|
||||
dispatch(userActions.setUpdateUserDrawerVisible(false));
|
||||
}
|
||||
|
||||
const onChange = (data: any) => {
|
||||
setFormUser({...formUser, ...data})
|
||||
}
|
||||
|
||||
const changesDetected = (): boolean => {
|
||||
return emailChanged() || nameChanged() || groupsChanged() || roleChanged()
|
||||
}
|
||||
|
||||
const emailChanged = (): boolean => {
|
||||
return formUser.email !== user.email
|
||||
}
|
||||
|
||||
const roleChanged = (): boolean => {
|
||||
return formUser.role !== user.role
|
||||
}
|
||||
|
||||
const nameChanged = (): boolean => {
|
||||
return formUser.name !== user.name
|
||||
}
|
||||
|
||||
const groupsChanged = (): boolean => {
|
||||
if (!formUser.autoGroupsNames) {
|
||||
return false
|
||||
}
|
||||
if (formUser.autoGroupsNames.length != user.auto_groups.length) {
|
||||
return true
|
||||
}
|
||||
const formGroupIds = groups?.filter(g => formUser.autoGroupsNames.includes(g.name)).map(g => g.id || '') || []
|
||||
|
||||
return user.auto_groups?.filter(g => !formGroupIds.includes(g)).length > 0
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{user &&
|
||||
<Drawer
|
||||
forceRender={true}
|
||||
headerStyle={{display: "none"}}
|
||||
open={updateUserDrawerVisible}
|
||||
bodyStyle={{paddingBottom: 80}}
|
||||
onClose={onCancel}
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button disabled={savedUser.loading} onClick={onCancel}>Cancel</Button>
|
||||
<Button type="primary" disabled={savedUser.loading || !changesDetected()}
|
||||
onClick={handleFormSubmit}>{`${formUser.id ? 'Save' : 'Invite'}`}</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form layout="vertical" hideRequiredMark form={form} onValuesChange={onChange}
|
||||
initialValues={{
|
||||
["role"]: formUser.role
|
||||
}}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Header style={{margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px"}}>
|
||||
<Row align="top">
|
||||
{/*Close Icon*/}
|
||||
<Col flex="none" style={{display: "flex"}}>
|
||||
{!editName && user.id &&
|
||||
<button type="button" aria-label="Close" className="ant-drawer-close"
|
||||
style={{paddingTop: 3}}
|
||||
onClick={onCancel}>
|
||||
<span role="img" aria-label="close"
|
||||
className="anticon anticon-close">
|
||||
<CloseOutlined size={16}/>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</Col>
|
||||
{/* Name Label*/}
|
||||
<Col flex="auto">
|
||||
{!editName && user.id && formUser.name !== "" ? (
|
||||
<div className={"access-control input-text ant-drawer-title"}
|
||||
onClick={() => toggleEditName(true)}>{formUser.name ? formUser.name : formUser.name}
|
||||
<EditOutlined/></div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Name"
|
||||
rules={[{
|
||||
required: false,
|
||||
message: 'Please add a new name for this user',
|
||||
whitespace: true
|
||||
}]}
|
||||
>
|
||||
<Input
|
||||
placeholder={formUser.name}
|
||||
ref={inputNameRef}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Header>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="email"
|
||||
label="Email"
|
||||
>
|
||||
<Input
|
||||
disabled={user.id}
|
||||
value={formUser.email}
|
||||
style={{color: "#5a5c5a"}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="role"
|
||||
label="Role"
|
||||
>
|
||||
<Select
|
||||
style={{width: '100%'}}
|
||||
disabled={currentUser.role != null && currentUser.role !== "admin"}>
|
||||
<Option value="admin">admin</Option>
|
||||
<Option value="user">user</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="autoGroupsNames"
|
||||
label="Auto-assigned groups"
|
||||
tooltip="Every peer enrolled with this user will be automatically added to these groups"
|
||||
rules={[{validator: selectValidator}]}
|
||||
>
|
||||
<Select mode="tags"
|
||||
style={{width: '100%'}}
|
||||
placeholder="Associate groups with the user"
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangeTags}
|
||||
disabled={currentUser.role != null && currentUser.role !== "admin"}
|
||||
dropdownRender={dropDownRender}
|
||||
>
|
||||
{
|
||||
tagGroups.map(m =>
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Divider></Divider>
|
||||
</Col>
|
||||
{currentUser && currentUser.role !== "admin" && (
|
||||
<div>
|
||||
<Col span={24}>
|
||||
<Alert
|
||||
message={<div style={{color: "#5a5c5a"}}>
|
||||
You are not an administrator, therefore you can't update users.</div>}
|
||||
showIcon={false}
|
||||
type="warning"/>
|
||||
</Col>
|
||||
<br></br>
|
||||
</div>
|
||||
)}
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
</Drawer>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserUpdate
|
||||
@@ -1,55 +0,0 @@
|
||||
import React, {useState} from 'react';
|
||||
|
||||
import { Button } from "antd";
|
||||
import TabSteps from "./TabSteps";
|
||||
import { StepCommand } from "./types"
|
||||
|
||||
export const LinuxTab = () => {
|
||||
|
||||
const [steps, setSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: 'Download and install Brew (package manager)',
|
||||
commands: (
|
||||
<Button type="primary" href="https://brew.sh/" target="_blank">Download Brew</Button>
|
||||
),
|
||||
copied: false
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 2,
|
||||
title: 'Install NetBird:',
|
||||
commands: [
|
||||
`# for CLI only`,
|
||||
`brew install netbirdio/tap/netbird`,
|
||||
`# for GUI package`,
|
||||
`brew install --cask netbirdio/tap/netbird-ui`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 3,
|
||||
title: 'Run NetBird and log in the browser:',
|
||||
commands: [
|
||||
`sudo netbird up`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 4,
|
||||
title: 'Get your IP address:',
|
||||
commands: [
|
||||
`sudo ifconfig utun100`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand
|
||||
])
|
||||
|
||||
return (
|
||||
<TabSteps stepsItems={steps}/>
|
||||
)
|
||||
}
|
||||
|
||||
export default LinuxTab
|
||||
@@ -1,82 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Button } from "antd";
|
||||
import TabSteps from "./TabSteps";
|
||||
import { StepCommand } from "./types"
|
||||
import { getConfig } from "../../config";
|
||||
import Paragraph from 'antd/lib/skeleton/Paragraph';
|
||||
const { grpcApiOrigin } = getConfig();
|
||||
|
||||
|
||||
export const UbuntuTab = () => {
|
||||
|
||||
const formatNetBirdUP = () => {
|
||||
let cmd = "sudo netbird up"
|
||||
if (grpcApiOrigin) {
|
||||
cmd = "sudo netbird up --management-url " + grpcApiOrigin
|
||||
}
|
||||
return [
|
||||
cmd
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
const [steps, setSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: 'Add repository:',
|
||||
commands: [
|
||||
`sudo apt install ca-certificates curl gnupg -y`,
|
||||
`curl -L https://pkgs.wiretrustee.com/debian/public.key | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/wiretrustee.gpg`,
|
||||
`echo 'deb https://pkgs.wiretrustee.com/debian stable main' | sudo tee /etc/apt/sources.list.d/wiretrustee.list`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 2,
|
||||
title: 'Install NetBird:',
|
||||
commands: [
|
||||
`sudo apt-get update`,
|
||||
`# for CLI only`,
|
||||
`sudo apt-get install netbird`,
|
||||
`# for GUI package`,
|
||||
`sudo apt-get install netbird-ui`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 3,
|
||||
title: 'Run NetBird and log in the browser:',
|
||||
commands: formatNetBirdUP(),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 4,
|
||||
title: 'Get your IP address:',
|
||||
commands: [
|
||||
`ip addr show wt0`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
])
|
||||
|
||||
/*const clickTest = () => {
|
||||
steps.push({
|
||||
key: steps.length+1,
|
||||
title: `Test ${steps.length+1}`,
|
||||
commands: [`hi lorena!`].join('\n'),
|
||||
copied: false
|
||||
})
|
||||
console.log(steps)
|
||||
setSteps([...steps])
|
||||
}*/
|
||||
|
||||
return (
|
||||
<TabSteps stepsItems={steps} />
|
||||
)
|
||||
}
|
||||
|
||||
export default UbuntuTab
|
||||
1
src/components/icons/docker_icon.svg
Normal file
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://docs.netbird.io/how-to/access-netbird-public-api">Learn more about personal access tokens</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>}
|
||||
{showPlainToken &&
|
||||
<Input style={{marginTop: "-15px", marginBottom: "25px"}} suffix={
|
||||
!tokenCopied ? <Button type="text" size="middle" className="btn-copy-code" icon={<CopyOutlined/>}
|
||||
style={{color: "rgb(107, 114, 128)", marginTop: "-1px"}}
|
||||
onClick={() => onCopyClick(plainToken, true)}/>
|
||||
: <Button type="text" size="middle" className="btn-copy-code" icon={<CheckOutlined/>}
|
||||
style={{color: "green", marginTop: "-1px"}}/>
|
||||
}
|
||||
defaultValue={plainToken}
|
||||
readOnly={true}
|
||||
></Input>}
|
||||
</Container>
|
||||
</Modal>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddPATPopup
|
||||
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
|
||||
316
src/components/popups/InviteUserPopup.tsx
Normal file
316
src/components/popups/InviteUserPopup.tsx
Normal file
@@ -0,0 +1,316 @@
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Typography
|
||||
} from "antd";
|
||||
import {Container} from "../Container";
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {useGetTokenSilently} from "../../utils/token";
|
||||
import {actions as userActions} from "../../store/user";
|
||||
import {actions as groupActions} from "../../store/group";
|
||||
import {User, UserToSave} from "../../store/user/types";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import {CustomTagProps} from "rc-select/lib/BaseSelect";
|
||||
import {QuestionCircleFilled} from "@ant-design/icons";
|
||||
|
||||
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://docs.netbird.io/how-to/access-netbird-public-api">Learn more about user</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Container>
|
||||
</Modal>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default InviteUserPopup
|
||||
101
src/components/popups/addpeer/addpeer/AddPeerPopup.tsx
Normal file
101
src/components/popups/addpeer/addpeer/AddPeerPopup.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React, {useState} from 'react';
|
||||
|
||||
import {Tabs, TabsProps} from "antd";
|
||||
import Icon, {AndroidFilled, AppleFilled, WindowsFilled} from "@ant-design/icons";
|
||||
import {ReactComponent as LinuxSVG} from "../../../icons/terminal_icon.svg";
|
||||
import UbuntuTab from "./UbuntuTab";
|
||||
import {ReactComponent as DockerSVG} from "../../../icons/docker_icon.svg";
|
||||
import Paragraph from "antd/lib/typography/Paragraph";
|
||||
import WindowsTab from "./WindowsTab";
|
||||
import MacTab from "./MacTab";
|
||||
import Link from "antd/lib/typography/Link";
|
||||
import DockerTab from "./DockerTab";
|
||||
import AndroidTab from "./AndroidTab";
|
||||
|
||||
type Props = {
|
||||
greeting?: string;
|
||||
headline: string;
|
||||
};
|
||||
|
||||
const detectOSTab = () => {
|
||||
let os = 1;
|
||||
if (navigator.userAgent.indexOf("Win") !== -1) os = 2;
|
||||
if (navigator.userAgent.indexOf("Mac") !== -1) os = 3;
|
||||
if (navigator.userAgent.indexOf("X11") !== -1) os = 1;
|
||||
if (navigator.userAgent.indexOf("Linux") !== -1) os = 1
|
||||
return os
|
||||
}
|
||||
|
||||
export const AddPeerPopup: React.FC<Props> = ({
|
||||
greeting,
|
||||
headline,
|
||||
}) => {
|
||||
|
||||
const [openTab, setOpenTab] = useState(detectOSTab);
|
||||
|
||||
const [width, setWidth] = useState<number>(window.innerWidth);
|
||||
const isMobile = width <= 768;
|
||||
|
||||
const items: TabsProps['items'] = [
|
||||
{
|
||||
key: "1",
|
||||
label: <span><Icon component={LinuxSVG}/>Linux</span>,
|
||||
children: <UbuntuTab/>,
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: <span><WindowsFilled/>Windows</span>,
|
||||
children: <WindowsTab/>,
|
||||
},
|
||||
{
|
||||
key: "3",
|
||||
label: <span><AppleFilled/>macOS</span>,
|
||||
children: <MacTab/>,
|
||||
},
|
||||
{
|
||||
key: "4",
|
||||
label: <span><AndroidFilled/>Android</span>,
|
||||
children: <AndroidTab/>,
|
||||
},
|
||||
{
|
||||
key: "5",
|
||||
label: <span><Icon component={DockerSVG}/>Docker</span>,
|
||||
children: <DockerTab/>,
|
||||
}
|
||||
];
|
||||
|
||||
return <>
|
||||
{greeting && <Paragraph
|
||||
style={{textAlign: "center", whiteSpace: "pre-line", fontSize: "2em", marginBottom: -10}}>
|
||||
{greeting}
|
||||
</Paragraph>}
|
||||
<Paragraph
|
||||
style={{textAlign: "center", whiteSpace: "pre-line", fontSize: "2em"}}>
|
||||
{headline}
|
||||
</Paragraph>
|
||||
<Paragraph type={"secondary"}
|
||||
style={{
|
||||
marginTop: "-15px",
|
||||
textAlign: "center",
|
||||
whiteSpace: "pre-line",
|
||||
}}>
|
||||
To get started install NetBird and log in using your {"\n"} email account.
|
||||
</Paragraph>
|
||||
|
||||
<Tabs centered={!isMobile}
|
||||
defaultActiveKey={openTab.toString()} tabPosition="top" animated={{inkBar: true, tabPane: false}}
|
||||
items={items}/>
|
||||
<Paragraph type={"secondary"}
|
||||
style={{
|
||||
marginTop: "15px",
|
||||
}}>
|
||||
After that you should be connected. Add more devices to your network or manage your existing devices in the
|
||||
admin panel.
|
||||
If you have further questions check out our {<Link target="_blank"
|
||||
href={"https://docs.netbird.io/how-to/getting-started#installation"}>installation
|
||||
guide</Link>}
|
||||
</Paragraph>
|
||||
</>
|
||||
}
|
||||
|
||||
export default AddPeerPopup
|
||||
51
src/components/popups/addpeer/addpeer/AndroidTab.tsx
Normal file
51
src/components/popups/addpeer/addpeer/AndroidTab.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React, {useState} from 'react';
|
||||
|
||||
import {Button, Image, Typography} from "antd";
|
||||
import TabSteps from "./TabSteps";
|
||||
import { StepCommand } from "./types"
|
||||
import googleplay from '../../../../assets/google-play-badge.png';
|
||||
const {Text} = Typography;
|
||||
|
||||
export const AndroidTab = () => {
|
||||
|
||||
const [steps, setSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: 'Download and install the application from Google Play Store:',
|
||||
commands: (
|
||||
<a href="https://play.google.com/store/apps/details?id=io.netbird.client" target="_blank">
|
||||
<Image width="12em" preview={false} style={{marginTop: "5px"}} src={googleplay}/>
|
||||
</a>
|
||||
),
|
||||
copied: false
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 2,
|
||||
title: 'Run the application and click on the "Connect" button in the middle of the screen',
|
||||
commands: '',
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
title: 'Sign up using your email address',
|
||||
commands: '',
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
}
|
||||
])
|
||||
|
||||
return (
|
||||
<div style={{marginTop: 10}}>
|
||||
<Text style={{fontWeight: "bold"}}>
|
||||
Install on Android
|
||||
</Text>
|
||||
<div style={{marginTop: 5}}>
|
||||
<TabSteps stepsItems={steps}/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AndroidTab
|
||||
63
src/components/popups/addpeer/addpeer/DockerTab.tsx
Normal file
63
src/components/popups/addpeer/addpeer/DockerTab.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React, {useState} from 'react';
|
||||
import {StepCommand} from "./types"
|
||||
import {formatDockerCommand, formatNetBirdUP} from "./common"
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import TabSteps from "./TabSteps";
|
||||
import {Button, Typography} from "antd";
|
||||
import Link from "antd/lib/typography/Link";
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
|
||||
export const DockerTab = () => {
|
||||
|
||||
const [steps, setSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: 'Install Docker',
|
||||
commands: (
|
||||
<Button style={{marginTop: "5px"}} type="primary" href="https://docs.docker.com/engine/install/" target="_blank">Official Docker website</Button>
|
||||
),
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 2,
|
||||
title: 'Run NetBird container',
|
||||
commands: formatDockerCommand(),
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 3,
|
||||
title: "Read docs",
|
||||
commands: (
|
||||
<Link href="https://docs.netbird.io/how-to/getting-started#running-net-bird-in-docker" target="_blank">Running NetBird in Docker</Link>
|
||||
),
|
||||
copied: false,
|
||||
showCopyButton: false
|
||||
} as StepCommand
|
||||
])
|
||||
|
||||
return (
|
||||
<div style={{marginTop: 10}}>
|
||||
{/*<Text style={{fontWeight: "bold"}}>
|
||||
Run in Docker
|
||||
</Text>
|
||||
<div style={{fontSize: ".85em", marginTop: 5, marginBottom: 25}}>
|
||||
<SyntaxHighlighter language="bash">
|
||||
{formatDockerCommand()}
|
||||
</SyntaxHighlighter>
|
||||
</div>*/}
|
||||
<Text style={{fontWeight: "bold"}}>
|
||||
Install on Ubuntu
|
||||
</Text>
|
||||
<div style={{marginTop: 5}}>
|
||||
<TabSteps stepsItems={steps}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default DockerTab
|
||||
@@ -12,7 +12,7 @@ export const OtherTab = () => {
|
||||
key: 1,
|
||||
title: 'For other installation options check our documentation.',
|
||||
commands: (
|
||||
<Button type="primary" href={`https://netbird.io/docs/getting-started/installation#binary-install`} target="_blank">
|
||||
<Button type="primary" href={`https://docs.netbird.io/how-to/getting-started#binary-install`} target="_blank">
|
||||
Documentation
|
||||
</Button>
|
||||
),
|
||||
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,8 +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"
|
||||
"silentRedirectURI": "$AUTH_SILENT_REDIRECT_URI",
|
||||
"tokenSource": "$NETBIRD_TOKEN_SOURCE"
|
||||
}
|
||||
@@ -9,7 +9,7 @@ if (process.env.NODE_ENV !== 'production') {
|
||||
|
||||
const defaultRedirectURI = '/#callback';
|
||||
const defaultSilentRedirectURI = '/#silent-callback'
|
||||
|
||||
const defaultTokenSource = "accessToken"
|
||||
export function getConfig() {
|
||||
let redirectURI = defaultRedirectURI
|
||||
if (configJson.redirectURI) {
|
||||
@@ -21,6 +21,11 @@ export function getConfig() {
|
||||
silentRedirectURI = configJson.silentRedirectURI
|
||||
}
|
||||
|
||||
let tokenSource = defaultTokenSource
|
||||
if (configJson.tokenSource) {
|
||||
tokenSource = configJson.tokenSource
|
||||
}
|
||||
|
||||
return {
|
||||
auth0Auth: configJson.auth0Auth == "true", //due to substitution we can't use boolean in the config
|
||||
authority: configJson.authAuthority,
|
||||
@@ -28,10 +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;
|
||||
}
|
||||
@@ -3,12 +3,6 @@ import axios from 'axios';
|
||||
import {ApiError, ApiRequestParams, ApiResponse} from './types';
|
||||
import {headersFactory, RequestHeader} from './header-factory';
|
||||
|
||||
/*axios.interceptors.response.use(undefined, err => {
|
||||
let res = err.response;
|
||||
if (res.status === 401) {
|
||||
}
|
||||
})*/
|
||||
|
||||
async function apiRequest<T>(params: ApiRequestParams): Promise<ApiResponse<T>> {
|
||||
const data = params.data ? (params.data as any).payload : undefined;
|
||||
const url = `${params.urlBase}${params.url}`;
|
||||
@@ -26,8 +20,10 @@ async function apiRequest<T>(params: ApiRequestParams): Promise<ApiResponse<T>>
|
||||
statusCode: -1
|
||||
};
|
||||
|
||||
let queryParams = (params.data as any).queryParams ? (params.data as any).queryParams : {};
|
||||
|
||||
try {
|
||||
response = await axios.request({url, data, method: params.method, headers: builtHeader as any});
|
||||
response = await axios.request({url, data, method: params.method, headers: builtHeader as any, params: queryParams});
|
||||
} catch (err: any) {
|
||||
error = <ApiError>{
|
||||
code: err ? err.code : '-1',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Method } from 'axios';
|
||||
|
||||
export interface RequestPayload<T> {
|
||||
getAccessTokenSilently: any | null;
|
||||
queryParams?: any | null;
|
||||
payload:T;
|
||||
}
|
||||
|
||||
@@ -27,6 +28,7 @@ export interface RequestConfig {
|
||||
export interface ApiRequestParams extends RequestConfig {
|
||||
method: Method;
|
||||
url: string;
|
||||
params?: any,
|
||||
data: unknown;
|
||||
urlBase: string;
|
||||
}
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
export enum StorageKey {
|
||||
token
|
||||
token,
|
||||
hadFirstRun
|
||||
}
|
||||
|
||||
const setLocalItem = async <T>(key: StorageKey, value: T): Promise<void> => {
|
||||
try {
|
||||
localStorage.setItem(`@net-bird:${key}`, JSON.stringify(value));
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
try {
|
||||
localStorage.setItem(`@net-bird:${key}`, JSON.stringify(value));
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
};
|
||||
|
||||
const getLocalItem = async <T>(key: StorageKey): Promise<T | null> => {
|
||||
try {
|
||||
const item = localStorage.getItem(`@net-bird:${key}`);
|
||||
if (!item) {
|
||||
return null;
|
||||
try {
|
||||
const item = localStorage.getItem(`@net-bird:${key}`);
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(item) as T;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
return JSON.parse(item) as T;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
return null;
|
||||
return null;
|
||||
};
|
||||
|
||||
export {
|
||||
getLocalItem,
|
||||
setLocalItem
|
||||
getLocalItem,
|
||||
setLocalItem
|
||||
}
|
||||
|
||||
23
src/store/account/actions.ts
Normal file
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 }
|
||||
}
|
||||
@@ -9,6 +9,10 @@ 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';
|
||||
@@ -27,5 +31,9 @@ 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 };
|
||||
@@ -59,7 +59,7 @@ export function* deletePeer(action: ReturnType<typeof actions.deletedPeer.reques
|
||||
} as DeleteResponse<string | null>));
|
||||
|
||||
const peers = (yield select(state => state.peer.data)) as Peer[]
|
||||
yield put(actions.getPeers.success(peers.filter((p:Peer) => p.ip !== action.payload.payload)))
|
||||
yield put(actions.getPeers.success(peers.filter((p:Peer) => p.id !== action.payload.payload)))
|
||||
} catch (err) {
|
||||
yield put(actions.deletedPeer.failure({
|
||||
loading: false,
|
||||
|
||||
@@ -14,6 +14,9 @@ export interface Peer {
|
||||
user_id?: string,
|
||||
ui_version?: string,
|
||||
dns_label: string,
|
||||
last_login: string,
|
||||
login_expired: boolean,
|
||||
login_expiration_enabled: boolean
|
||||
}
|
||||
|
||||
export interface FormPeer extends Peer {
|
||||
@@ -40,6 +43,10 @@ export interface PeerIPToName {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface PeerIPToID {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface PeerDataTable extends Peer {
|
||||
key: string;
|
||||
groups: Group[];
|
||||
|
||||
41
src/store/personal-access-token/actions.ts
Normal file
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,10 +1,14 @@
|
||||
import { actions as PeerActions } from './peer';
|
||||
import { actions as SetupKeyActions } from './setup-key';
|
||||
import { actions as UserActions } from './user';
|
||||
import { actions as GroupActions } from './group';
|
||||
import { actions as RuleActions } from './rule';
|
||||
import { actions as RouteActions } from './route';
|
||||
import { actions as NameServerGroupActions } from './nameservers';
|
||||
import {actions as PeerActions} from './peer';
|
||||
import {actions as SetupKeyActions} from './setup-key';
|
||||
import {actions as UserActions} from './user';
|
||||
import {actions as GroupActions} from './group';
|
||||
import {actions as RuleActions} from './rule';
|
||||
import {actions as RouteActions} from './route';
|
||||
import {actions as NameServerGroupActions} from './nameservers';
|
||||
import {actions as EventActions} from './event';
|
||||
import {actions as DNSSettingsActions} from './dns-settings';
|
||||
import {actions as AccountActions} from './account';
|
||||
import {actions as PersonalAccessTokenActions} from './personal-access-token';
|
||||
|
||||
export default {
|
||||
peer: PeerActions,
|
||||
@@ -13,5 +17,9 @@ export default {
|
||||
group: GroupActions,
|
||||
rule: RuleActions,
|
||||
route: RouteActions,
|
||||
nameserverGroup: NameServerGroupActions
|
||||
nameserverGroup: NameServerGroupActions,
|
||||
event: EventActions,
|
||||
dnsSettings: DNSSettingsActions,
|
||||
account: AccountActions,
|
||||
personalAccessToken: PersonalAccessTokenActions
|
||||
};
|
||||
|
||||
@@ -7,6 +7,10 @@ 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,
|
||||
@@ -15,5 +19,9 @@ export default combineReducers({
|
||||
group,
|
||||
rule,
|
||||
route,
|
||||
nameserverGroup
|
||||
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[]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import {ExpiresInValue} from "../../views/ExpiresInInput";
|
||||
import moment from "moment";
|
||||
|
||||
export interface SetupKey {
|
||||
expires: string;
|
||||
@@ -19,6 +20,8 @@ export interface SetupKey {
|
||||
export interface FormSetupKey extends SetupKey {
|
||||
autoGroupNames: string[]
|
||||
expiresInFormatted: ExpiresInValue
|
||||
exp: moment.Moment
|
||||
last: moment.Moment
|
||||
}
|
||||
|
||||
export interface SetupKeyToSave extends SetupKey
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {ActionType, createAction, createAsyncAction} from 'typesafe-actions';
|
||||
import {User, UserToSave} from './types';
|
||||
import {ApiError, CreateResponse, RequestPayload} from '../../services/api-client/types';
|
||||
import {ApiError, CreateResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
|
||||
|
||||
const actions = {
|
||||
getUsers: createAsyncAction(
|
||||
@@ -9,10 +9,38 @@ const actions = {
|
||||
'GET_USERS_FAILURE',
|
||||
)<RequestPayload<null>, User[], ApiError>(),
|
||||
|
||||
getServiceUsers: createAsyncAction(
|
||||
'GET_SERVICE_USERS_REQUEST',
|
||||
'GET_SERVICE_USERS_SUCCESS',
|
||||
'GET_SERVICE_USERS_FAILURE',
|
||||
)<RequestPayload<null>, User[], ApiError>(),
|
||||
|
||||
getRegularUsers: createAsyncAction(
|
||||
'GET_REGULAR_USERS_REQUEST',
|
||||
'GET_REGULAR_USERS_SUCCESS',
|
||||
'GET_REGULAR_USERS_FAILURE',
|
||||
)<RequestPayload<null>, User[], ApiError>(),
|
||||
|
||||
deleteUser: createAsyncAction(
|
||||
'DELETE_USER_REQUEST',
|
||||
'DELETE_USER_SUCCESS',
|
||||
'DELETE_USER_FAILURE',
|
||||
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
|
||||
setDeletedUser: createAction('SET_DELETED_USER')<DeleteResponse<string | null>>(),
|
||||
resetDeletedUser: createAction('RESET_DELETED_USER')<null>(),
|
||||
|
||||
// used to set a user object that was picked in the user table in the UserUpdate drawer (user update window on right-side).
|
||||
setUser: createAction('SET_USER')<User>(),
|
||||
// used to make the UserUpdate drawer visible in the UI.
|
||||
setUpdateUserDrawerVisible: createAction('SET_UPDATE_USER_VISIBLE')<boolean>(),
|
||||
// used to make the ViewUserPopup visible in the UI.
|
||||
setInviteUserPopupVisible: createAction('SET_INVITE_USER_VISIBLE')<boolean>(),
|
||||
// used to make the EditUserPopup visible in the UI.
|
||||
setEditUserPopupVisible: createAction('SET_EDIT_USER_VISIBLE')<boolean>(),
|
||||
// used to make the AddServiceUserPopup visible in the UI.
|
||||
setAddServiceUserPopupVisible: createAction('SET_ADD_SERVICE_USER_VISIBLE')<boolean>(),
|
||||
// used to remember what tab was open on users page
|
||||
setUserTabOpen: createAction('SET_USER_TAB_OPEN')<string>(),
|
||||
|
||||
saveUser: createAsyncAction(
|
||||
'SAVE_USER_REQUEST',
|
||||
|
||||
@@ -2,24 +2,44 @@ import { createReducer } from 'typesafe-actions';
|
||||
import { combineReducers } from 'redux';
|
||||
import { User } from './types';
|
||||
import actions, { ActionTypes } from './actions';
|
||||
import {ApiError, CreateResponse} from "../../services/api-client/types";
|
||||
import {ApiError, CreateResponse, DeleteResponse} from "../../services/api-client/types";
|
||||
|
||||
type StateType = Readonly<{
|
||||
data: User[] | null;
|
||||
serviceUsers: User[] | null;
|
||||
regularUsers: User[] | null;
|
||||
loading: boolean;
|
||||
failed: ApiError | null;
|
||||
user: User | null;
|
||||
deletedUser: DeleteResponse<string | null>;
|
||||
savedUser: CreateResponse<User | null>;
|
||||
updateUserDrawerVisible: boolean
|
||||
editUserPopupVisible: boolean
|
||||
inviteUserPopupVisible: boolean
|
||||
addServiceUserPopupVisible: boolean
|
||||
usersTabOpen: string
|
||||
}>;
|
||||
|
||||
const initialState: StateType = {
|
||||
data: [],
|
||||
serviceUsers: [],
|
||||
regularUsers: [],
|
||||
loading: false,
|
||||
failed: null,
|
||||
user: null,
|
||||
deletedUser: <DeleteResponse<string | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
failure: false,
|
||||
error: null,
|
||||
data : null
|
||||
},
|
||||
// right-sided user update drawer
|
||||
updateUserDrawerVisible: false,
|
||||
editUserPopupVisible: false,
|
||||
inviteUserPopupVisible: false,
|
||||
addServiceUserPopupVisible: false,
|
||||
usersTabOpen: 'Users',
|
||||
savedUser: <CreateResponse<User | null>>{
|
||||
loading: false,
|
||||
success: false,
|
||||
@@ -33,6 +53,14 @@ const data = createReducer<User[], ActionTypes>(initialState.data as User[])
|
||||
.handleAction(actions.getUsers.success,(_, action) => action.payload)
|
||||
.handleAction(actions.getUsers.failure, () => []);
|
||||
|
||||
const serviceUsers = createReducer<User[], ActionTypes>(initialState.serviceUsers as User[])
|
||||
.handleAction(actions.getServiceUsers.success,(_, action) => action.payload)
|
||||
.handleAction(actions.getServiceUsers.failure, () => []);
|
||||
|
||||
const regularUsers = createReducer<User[], ActionTypes>(initialState.regularUsers as User[])
|
||||
.handleAction(actions.getRegularUsers.success,(_, action) => action.payload)
|
||||
.handleAction(actions.getRegularUsers.failure, () => []);
|
||||
|
||||
const loading = createReducer<boolean, ActionTypes>(initialState.loading)
|
||||
.handleAction(actions.getUsers.request, () => true)
|
||||
.handleAction(actions.getUsers.success, () => false)
|
||||
@@ -45,9 +73,29 @@ const failed = createReducer<ApiError | null, ActionTypes>(initialState.failed)
|
||||
|
||||
const user = createReducer<User, ActionTypes>(initialState.user as User)
|
||||
.handleAction(actions.setUser, (store, action) => action.payload);
|
||||
|
||||
const deletedUser = createReducer<DeleteResponse<string | null>, ActionTypes>(initialState.deletedUser)
|
||||
.handleAction(actions.deleteUser.request, () => initialState.deletedUser)
|
||||
.handleAction(actions.deleteUser.success, (store, action) => action.payload)
|
||||
.handleAction(actions.deleteUser.failure, (store, action) => action.payload)
|
||||
.handleAction(actions.setDeletedUser, (store, action) => action.payload)
|
||||
.handleAction(actions.resetDeletedUser, (store, action) => initialState.deletedUser);
|
||||
|
||||
const updateUserDrawerVisible = createReducer<boolean, ActionTypes>(initialState.updateUserDrawerVisible)
|
||||
.handleAction(actions.setUpdateUserDrawerVisible, (store, action) => action.payload);
|
||||
|
||||
const inviteUserPopupVisible = createReducer<boolean, ActionTypes>(initialState.inviteUserPopupVisible)
|
||||
.handleAction(actions.setInviteUserPopupVisible, (store, action) => action.payload);
|
||||
|
||||
const editUserPopupVisible = createReducer<boolean, ActionTypes>(initialState.editUserPopupVisible)
|
||||
.handleAction(actions.setEditUserPopupVisible, (store, action) => action.payload);
|
||||
|
||||
const addServiceUserPopupVisible = createReducer<boolean, ActionTypes>(initialState.addServiceUserPopupVisible)
|
||||
.handleAction(actions.setAddServiceUserPopupVisible, (store, action) => action.payload);
|
||||
|
||||
const userTabOpen = createReducer<string, ActionTypes>(initialState.usersTabOpen)
|
||||
.handleAction(actions.setUserTabOpen, (store, action) => action.payload);
|
||||
|
||||
const savedUser = createReducer<CreateResponse<User | null>, ActionTypes>(initialState.savedUser)
|
||||
.handleAction(actions.saveUser.request, () => initialState.savedUser)
|
||||
.handleAction(actions.saveUser.success, (store, action) => action.payload)
|
||||
@@ -57,9 +105,16 @@ const savedUser = createReducer<CreateResponse<User | null>, ActionTypes>(initia
|
||||
|
||||
export default combineReducers({
|
||||
data,
|
||||
serviceUsers,
|
||||
regularUsers,
|
||||
loading,
|
||||
failed,
|
||||
user,
|
||||
savedUser,
|
||||
updateUserDrawerVisible
|
||||
deletedUser,
|
||||
updateUserDrawerVisible,
|
||||
inviteUserPopupVisible,
|
||||
editUserPopupVisible,
|
||||
addServiceUserPopupVisible,
|
||||
userTabOpen
|
||||
});
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import {all, call, put, takeLatest} from 'redux-saga/effects';
|
||||
import {ApiError, ApiResponse, CreateResponse} from '../../services/api-client/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";
|
||||
|
||||
export function* getUsers(action: ReturnType<typeof actions.getUsers.request>): Generator {
|
||||
try {
|
||||
@@ -18,6 +17,30 @@ export function* getUsers(action: ReturnType<typeof actions.getUsers.request>):
|
||||
}
|
||||
}
|
||||
|
||||
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({
|
||||
@@ -49,11 +72,12 @@ export function* saveUser(action: ReturnType<typeof actions.saveUser.request>):
|
||||
email: userToSave.email,
|
||||
role: userToSave.role,
|
||||
auto_groups: newGroups,
|
||||
is_service_user: userToSave.is_service_user,
|
||||
is_blocked: userToSave.is_blocked
|
||||
} as UserToSave
|
||||
|
||||
let effect
|
||||
if (!userToSave.id) {
|
||||
console.log("creating user:" + payload)
|
||||
effect = yield call(service.createUser, {
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: payload
|
||||
@@ -74,15 +98,6 @@ export function* saveUser(action: ReturnType<typeof actions.saveUser.request>):
|
||||
error: null,
|
||||
data: response.body
|
||||
} as CreateResponse<User | null>));
|
||||
|
||||
yield put(groupActions.getGroups.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
yield put(actions.getUsers.request({
|
||||
getAccessTokenSilently: action.payload.getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
} catch (err) {
|
||||
yield put(actions.saveUser.failure({
|
||||
loading: false,
|
||||
@@ -94,10 +109,51 @@ export function* saveUser(action: ReturnType<typeof actions.saveUser.request>):
|
||||
}
|
||||
}
|
||||
|
||||
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, getUsers),
|
||||
takeLatest(actions.saveUser.request, saveUser)
|
||||
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, UserToSave} from './types';
|
||||
import {SpecificPAT} from "../personal-access-token/types";
|
||||
|
||||
export default {
|
||||
async getUsers(payload:RequestPayload<null>): Promise<ApiResponse<User[]>> {
|
||||
@@ -25,4 +26,10 @@ export default {
|
||||
payload
|
||||
);
|
||||
},
|
||||
async deleteUser(payload:RequestPayload<string>): Promise<ApiResponse<any>> {
|
||||
return apiClient.delete<any>(
|
||||
`/api/users/` + payload.payload,
|
||||
payload
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,15 +3,18 @@ export interface User {
|
||||
email?: string;
|
||||
name: string;
|
||||
role: string;
|
||||
status: string
|
||||
auto_groups: string[]
|
||||
status: string;
|
||||
auto_groups: string[];
|
||||
is_current?: boolean;
|
||||
is_service_user?: boolean;
|
||||
is_blocked?: boolean;
|
||||
}
|
||||
|
||||
export interface FormUser extends User {
|
||||
autoGroupsNames: string[]
|
||||
is_active?: boolean
|
||||
}
|
||||
|
||||
export interface UserToSave extends User
|
||||
{
|
||||
export interface UserToSave extends User {
|
||||
groupsToCreate: string[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
|
||||
|
||||
export const formatOS = (os) => {
|
||||
if (os.startsWith("windows 10")) {
|
||||
return "Windows 10";
|
||||
@@ -6,7 +8,8 @@ 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;
|
||||
};
|
||||
|
||||
@@ -17,6 +20,27 @@ export const formatDate = date => {
|
||||
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(' ')
|
||||
}
|
||||
@@ -53,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;
|
||||
@@ -69,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`;
|
||||
@@ -78,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
|
||||
}
|
||||
|
||||
@@ -94,12 +94,16 @@ export const useGetGroupTagHelpers = () => {
|
||||
return groups?.filter(g => groupIDList.includes(g.id!)).map(g => g.name || '') || []
|
||||
}
|
||||
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = []
|
||||
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)
|
||||
@@ -131,6 +135,7 @@ export const useGetGroupTagHelpers = () => {
|
||||
setGroupTagFilterAll,
|
||||
getExistingAndToCreateGroupsLists,
|
||||
getGroupNamesFromIDs,
|
||||
selectValidator
|
||||
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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useOidcAccessToken} from "@axa-fr/react-oidc";
|
||||
import {useEffect} from "react";
|
||||
import {useOidcAccessToken, useOidcIdToken} from "@axa-fr/react-oidc";
|
||||
import {createRef, useEffect} from "react";
|
||||
|
||||
function sleep(ms : number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
@@ -32,10 +32,8 @@ let latestToken:string
|
||||
|
||||
// hook that returns a getAccessTokenSilently function that returns an access token promise,
|
||||
// waiting for renewal if it was expired
|
||||
export const useGetAccessTokenSilently = () => {
|
||||
const {accessToken} = useOidcAccessToken()
|
||||
latestToken = accessToken
|
||||
const getAccessTokenSilently = async (): Promise<string> => {
|
||||
export const useGetTokenSilently = () => {
|
||||
const getTokenSilently = async (): Promise<string> => {
|
||||
let attempt = 0
|
||||
while (!isTokenValid(latestToken) && attempt < 15){
|
||||
attempt++
|
||||
@@ -45,9 +43,29 @@ export const useGetAccessTokenSilently = () => {
|
||||
return latestToken
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
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])
|
||||
|
||||
return {getAccessTokenSilently}
|
||||
}
|
||||
useEffect(() => {
|
||||
if (source.toLowerCase() == "idtoken") {
|
||||
latestToken = idToken
|
||||
}
|
||||
}, [idToken])
|
||||
}
|
||||
@@ -27,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';
|
||||
@@ -35,7 +35,9 @@ import AccessControlNew from "../components/AccessControlNew";
|
||||
import {Group} from "../store/group/types";
|
||||
import AccessControlModalGroups from "../components/AccessControlModalGroups";
|
||||
import tableSpin from "../components/Spin";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {usePageSizeHelpers} from "../utils/pageSize";
|
||||
import {PeerDataTable} from "../store/peer/types";
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
const {Column} = Table;
|
||||
@@ -56,7 +58,8 @@ interface GroupsToShow {
|
||||
}
|
||||
|
||||
export const AccessControl = () => {
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const rules = useSelector((state: RootState) => state.rule.data);
|
||||
@@ -68,19 +71,13 @@ export const AccessControl = () => {
|
||||
const [showTutorial, setShowTutorial] = useState(true)
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [optionAllEnable, setOptionAllEnable] = useState('enabled');
|
||||
const [pageSize, setPageSize] = useState(5);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [dataTable, setDataTable] = useState([] as RuleDataTable[]);
|
||||
const [ruleToAction, setRuleToAction] = useState(null as RuleDataTable | null);
|
||||
const [groupsToShow, setGroupsToShow] = useState({} as GroupsToShow)
|
||||
const setupNewRuleVisible = useSelector((state: RootState) => state.rule.setupNewRuleVisible);
|
||||
const [groupPopupVisible, setGroupPopupVisible] = useState(false as boolean | undefined)
|
||||
const [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'}]
|
||||
|
||||
@@ -123,8 +120,8 @@ export const AccessControl = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(ruleActions.getRules.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
dispatch(ruleActions.getRules.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -200,26 +197,19 @@ export const AccessControl = () => {
|
||||
setOptionAllEnable(value)
|
||||
}
|
||||
|
||||
const onChangePageSize = (value: string) => {
|
||||
setPageSize(parseInt(value.toString()))
|
||||
}
|
||||
|
||||
const showConfirmDelete = () => {
|
||||
let name = ruleToAction ? ruleToAction.name : '';
|
||||
confirm({
|
||||
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 this rule 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: getAccessTokenSilently,
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: ruleToAction?.id || ''
|
||||
}));
|
||||
},
|
||||
@@ -310,15 +300,19 @@ export const AccessControl = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (setupNewRuleVisible) {
|
||||
setGroupPopupVisible(false)
|
||||
setGroupPopupVisible("")
|
||||
}
|
||||
}, [setupNewRuleVisible])
|
||||
|
||||
const onPopoverVisibleChange = (b: boolean) => {
|
||||
const onPopoverVisibleChange = (b: boolean, key: string) => {
|
||||
if (setupNewRuleVisible) {
|
||||
setGroupPopupVisible(false)
|
||||
setGroupPopupVisible("")
|
||||
} else {
|
||||
setGroupPopupVisible(undefined)
|
||||
if (b) {
|
||||
setGroupPopupVisible(key)
|
||||
} else {
|
||||
setGroupPopupVisible("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,7 +321,6 @@ export const AccessControl = () => {
|
||||
const _g = g as Group
|
||||
const peersCount = ` - ${_g.peers_count || 0} ${(!_g.peers_count || parseInt(_g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<Space direction="vertical">
|
||||
<div key={i}>
|
||||
<Tag
|
||||
color="blue"
|
||||
@@ -337,14 +330,14 @@ export const AccessControl = () => {
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</div>
|
||||
</Space>
|
||||
)
|
||||
})
|
||||
const mainContent = (<Space direction="vertical">{content}</Space>)
|
||||
return (
|
||||
<Popover
|
||||
onOpenChange={onPopoverVisibleChange}
|
||||
open={groupPopupVisible}
|
||||
content={content}
|
||||
onOpenChange={(b: boolean) => onPopoverVisibleChange(b, rule.key)}
|
||||
open={groupPopupVisible === rule.key}
|
||||
content={mainContent}
|
||||
title={null}>
|
||||
<Button type="link" onClick={() => setRuleAndView(rule)}>{label}</Button>
|
||||
</Popover>
|
||||
@@ -457,11 +450,17 @@ export const AccessControl = () => {
|
||||
<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>
|
||||
|
||||
311
src/views/Activity.tsx
Normal file
311
src/views/Activity.tsx
Normal file
@@ -0,0 +1,311 @@
|
||||
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":
|
||||
case "user.block":
|
||||
case "user.unblock":
|
||||
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://docs.netbird.io/how-to/monitor-system-and-network-activity">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;
|
||||
|
||||
|
||||
@@ -1,350 +1,58 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as nsGroupActions} from '../store/nameservers';
|
||||
import {Container} from "../components/Container";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Dropdown,
|
||||
Input,
|
||||
Menu,
|
||||
message,
|
||||
Modal,
|
||||
Popover,
|
||||
Radio,
|
||||
RadioChangeEvent,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Tabs,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {filter} from "lodash";
|
||||
import tableSpin from "../components/Spin";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {actions as groupActions} from "../store/group";
|
||||
import {Group} from "../store/group/types";
|
||||
import {TooltipPlacement} from "antd/es/tooltip";
|
||||
import {NameServer, NameServerGroup} from "../store/nameservers/types";
|
||||
import type { TabsProps } from 'antd';
|
||||
import NameServerGroupUpdate from "../components/NameServerGroupUpdate";
|
||||
import {ExclamationCircleOutlined} from "@ant-design/icons";
|
||||
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;
|
||||
const {Column} = Table;
|
||||
const {confirm} = Modal;
|
||||
|
||||
interface NameserverGroupDataTable extends NameServerGroup {
|
||||
key: string
|
||||
}
|
||||
|
||||
const styleNotification = {marginTop: 85}
|
||||
|
||||
export const DNS = () => {
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const {
|
||||
getGroupNamesFromIDs,
|
||||
} = useGetGroupTagHelpers()
|
||||
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const nsGroup = useSelector((state: RootState) => state.nameserverGroup.data);
|
||||
const failed = useSelector((state: RootState) => state.nameserverGroup.failed);
|
||||
const loading = useSelector((state: RootState) => state.nameserverGroup.loading);
|
||||
const updateNameServerGroupVisible = useSelector((state: RootState) => state.nameserverGroup.setupNewNameServerGroupVisible)
|
||||
const savedNSGroup = useSelector((state: RootState) => state.nameserverGroup.savedNameServerGroup)
|
||||
|
||||
const [groupPopupVisible, setGroupPopupVisible] = useState(false as boolean | undefined)
|
||||
const [nsGroupToAction, setNsGroupToAction] = useState(null as NameserverGroupDataTable | null);
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [optionAllEnable, setOptionAllEnable] = useState('enabled');
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [dataTable, setDataTable] = useState([] as NameserverGroupDataTable[]);
|
||||
const [showTutorial, setShowTutorial] = useState(false)
|
||||
const pageSizeOptions = [
|
||||
{label: "5", value: "5"},
|
||||
{label: "10", value: "10"},
|
||||
{label: "15", value: "15"}
|
||||
]
|
||||
|
||||
const optionsAllEnabled = [{label: 'Enabled', value: 'enabled'}, {label: 'All', value: 'all'}]
|
||||
|
||||
// setUserAndView makes the UserUpdate drawer visible (right side) and sets the user object
|
||||
const setUserAndView = (nsGroup: NameServerGroup) => {
|
||||
dispatch(nsGroupActions.setSetupNewNameServerGroupVisible(true));
|
||||
dispatch(nsGroupActions.setNameServerGroup({
|
||||
id: nsGroup.id,
|
||||
name: nsGroup.name,
|
||||
primary: nsGroup.primary,
|
||||
domains: nsGroup.domains,
|
||||
description: nsGroup.description,
|
||||
nameservers: nsGroup.nameservers,
|
||||
groups: nsGroup.groups,
|
||||
enabled: nsGroup.enabled,
|
||||
} as NameServerGroup));
|
||||
}
|
||||
|
||||
const transformDataTable = (d: NameServerGroup[]): NameserverGroupDataTable[] => {
|
||||
return d.map(p => ({key: p.id, ...p} as NameserverGroupDataTable))
|
||||
}
|
||||
const dnsSettingsData = useSelector((state: RootState) => state.dnsSettings.data)
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(nsGroupActions.getNameServerGroups.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (nsGroup.length > 0) {
|
||||
setShowTutorial(false)
|
||||
} else {
|
||||
setShowTutorial(true)
|
||||
}
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}, [nsGroup])
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}, [textToSearch, optionAllEnable])
|
||||
|
||||
const filterDataTable = (): NameServerGroup[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let f = filter(nsGroup, (f: NameServerGroup) =>
|
||||
((f.name).toLowerCase().includes(t) ||
|
||||
f.name.includes(t) || t === "" ||
|
||||
getGroupNamesFromIDs(f.groups).find(u => u.toLowerCase().trim().includes(t)) ||
|
||||
f.domains.find(d => d.toLowerCase().trim().includes(t)) ||
|
||||
f.nameservers.find(n => n.ip.includes(t)))
|
||||
) as NameServerGroup[]
|
||||
if (optionAllEnable !== "all") {
|
||||
f = filter(f, (f) => f.enabled)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
const onChangeAllEnabled = ({target: {value}}: RadioChangeEvent) => {
|
||||
setOptionAllEnable(value)
|
||||
}
|
||||
|
||||
const onChangeTextToSearch = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setTextToSearch(e.target.value)
|
||||
};
|
||||
|
||||
const searchDataTable = () => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}
|
||||
|
||||
const onChangePageSize = (value: string) => {
|
||||
setPageSize(parseInt(value.toString()))
|
||||
}
|
||||
|
||||
const onClickEdit = () => {
|
||||
dispatch(nsGroupActions.setSetupNewNameServerGroupVisible(true));
|
||||
dispatch(nsGroupActions.setNameServerGroup({
|
||||
id: nsGroupToAction?.id,
|
||||
name: nsGroupToAction?.name,
|
||||
primary: nsGroupToAction?.primary,
|
||||
domains: nsGroupToAction?.domains,
|
||||
description: nsGroupToAction?.description,
|
||||
groups: nsGroupToAction?.groups,
|
||||
enabled: nsGroupToAction?.enabled,
|
||||
nameservers: nsGroupToAction?.nameservers,
|
||||
} as NameServerGroup));
|
||||
}
|
||||
|
||||
const showConfirmDelete = () => {
|
||||
confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
width: 600,
|
||||
content: <Space direction="vertical" size="small">
|
||||
{nsGroupToAction &&
|
||||
<>
|
||||
<Title level={5}>Delete Nameserver group "{nsGroupToAction ? nsGroupToAction.name : ''}"</Title>
|
||||
<Paragraph>Are you sure you want to delete this nameserver group from your account?</Paragraph>
|
||||
</>
|
||||
}
|
||||
</Space>,
|
||||
okType: 'danger',
|
||||
onOk() {
|
||||
dispatch(nsGroupActions.deleteNameServerGroup.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: nsGroupToAction?.id || ''
|
||||
}));
|
||||
},
|
||||
onCancel() {
|
||||
setNsGroupToAction(null);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const renderPopoverGroups = (label: string, rowGroups: string[] | null, userToAction: NameserverGroupDataTable) => {
|
||||
|
||||
let groupsMap = new Map<string, Group>();
|
||||
groups.forEach(g => {
|
||||
groupsMap.set(g.id!, g)
|
||||
})
|
||||
|
||||
let displayGroups: Group[] = []
|
||||
if (rowGroups) {
|
||||
displayGroups = rowGroups.filter(g => groupsMap.get(g)).map(g => groupsMap.get(g)!)
|
||||
}
|
||||
|
||||
let btn = <Button type="link" onClick={() => setUserAndView(userToAction)}>{displayGroups.length}</Button>
|
||||
if (!displayGroups || displayGroups!.length < 1) {
|
||||
return btn
|
||||
}
|
||||
|
||||
const content = displayGroups?.map((g, i) => {
|
||||
const _g = g as Group
|
||||
const peersCount = ` - ${_g.peers_count || 0} ${(!_g.peers_count || parseInt(_g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<div key={i}>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{_g.name}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
const mainContent = (<Space direction="vertical">{content}</Space>)
|
||||
let popoverPlacement = "top"
|
||||
if (content && content.length > 5) {
|
||||
popoverPlacement = "rightTop"
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover placement={popoverPlacement as TooltipPlacement}
|
||||
key={userToAction.id}
|
||||
onOpenChange={onPopoverVisibleChange}
|
||||
open={groupPopupVisible}
|
||||
content={mainContent}
|
||||
title={null}>
|
||||
{btn}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
const renderPopoverDomains = (_: string, inputDomains: string[] | null, userToAction: NameserverGroupDataTable) => {
|
||||
var domains = [] as string[]
|
||||
if (inputDomains?.length) {
|
||||
domains = inputDomains
|
||||
}
|
||||
|
||||
let btn = <Button type="link"
|
||||
onClick={() => setUserAndView(userToAction)}>{domains.length ? domains.length : 0}</Button>
|
||||
if (!domains || domains!.length < 1) {
|
||||
return btn
|
||||
}
|
||||
|
||||
const content = domains?.map((d, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{d}</strong>
|
||||
</Tag>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const mainContent = (<Space direction="vertical">{content}</Space>)
|
||||
let popoverPlacement = "top"
|
||||
if (content && content.length > 5) {
|
||||
popoverPlacement = "rightTop"
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover placement={popoverPlacement as TooltipPlacement}
|
||||
key={userToAction.id}
|
||||
onOpenChange={onPopoverVisibleChange}
|
||||
open={groupPopupVisible}
|
||||
content={mainContent}
|
||||
title={null}>
|
||||
{btn}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (updateNameServerGroupVisible) {
|
||||
setGroupPopupVisible(false)
|
||||
}
|
||||
}, [updateNameServerGroupVisible])
|
||||
|
||||
const createKey = 'saving';
|
||||
useEffect(() => {
|
||||
if (savedNSGroup.loading) {
|
||||
message.loading({content: 'Saving...', key: createKey, duration: 0, style: styleNotification});
|
||||
} else if (savedNSGroup.success) {
|
||||
message.success({
|
||||
content: 'User has been successfully saved.',
|
||||
key: createKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(nsGroupActions.setSetupNewNameServerGroupVisible(false));
|
||||
dispatch(nsGroupActions.setSavedNameServerGroup({...savedNSGroup, success: false}));
|
||||
dispatch(nsGroupActions.resetSavedNameServerGroup(null))
|
||||
} else if (savedNSGroup.error) {
|
||||
let errorMsg = "Failed to update nameserver group"
|
||||
switch (savedNSGroup.error.statusCode) {
|
||||
case 403:
|
||||
errorMsg = "Failed to update user. You might not have enough permissions."
|
||||
break
|
||||
default:
|
||||
errorMsg = savedNSGroup.error.data.message ? savedNSGroup.error.data.message : errorMsg
|
||||
break
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: createKey,
|
||||
duration: 5,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(nsGroupActions.setSavedNameServerGroup({...savedNSGroup, error: null}));
|
||||
dispatch(nsGroupActions.resetSavedNameServerGroup(null))
|
||||
}
|
||||
}, [savedNSGroup])
|
||||
|
||||
const onPopoverVisibleChange = () => {
|
||||
if (updateNameServerGroupVisible) {
|
||||
setGroupPopupVisible(false)
|
||||
} else {
|
||||
setGroupPopupVisible(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
const itemsMenuAction = [
|
||||
const nsTabKey = '1'
|
||||
const items: TabsProps['items'] = [
|
||||
{
|
||||
key: "edit",
|
||||
label: (<Button type="text" onClick={() => onClickEdit()}>View</Button>)
|
||||
key: nsTabKey,
|
||||
label: 'Nameservers',
|
||||
children: <Nameservers/>,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: (<Button type="text" onClick={() => showConfirmDelete()}>Delete</Button>)
|
||||
key: '2',
|
||||
label: 'Settings',
|
||||
children: <DNSSettingsForm/>,
|
||||
},
|
||||
]
|
||||
|
||||
const actionsMenu = (<Menu items={itemsMenuAction}></Menu>)
|
||||
|
||||
const onClickAddNewNSGroup = () => {
|
||||
dispatch(nsGroupActions.setSetupNewNameServerGroupVisible(true));
|
||||
dispatch(nsGroupActions.setNameServerGroup({
|
||||
enabled: true,
|
||||
primary: true,
|
||||
} as NameServerGroup))
|
||||
const onTabClick = (key:string) => {
|
||||
if (key == nsTabKey) {
|
||||
if (!dnsSettingsData) return
|
||||
dispatch(dnsSettingsActions.setDNSSettings({
|
||||
disabled_management_groups: getGroupNamesFromIDs(dnsSettingsData.disabled_management_groups),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -352,127 +60,13 @@ export const DNS = () => {
|
||||
<Container style={{paddingTop: "40px"}}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Nameservers</Title>
|
||||
<Paragraph>Add nameservers for domain name resolution in your NetBird network</Paragraph>
|
||||
<Space direction="vertical" size="large" style={{display: 'flex'}}>
|
||||
<Row gutter={[16, 24]}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
|
||||
<Input allowClear value={textToSearch} onPressEnter={searchDataTable}
|
||||
placeholder="Search..." onChange={onChangeTextToSearch}/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
|
||||
<Space size="middle">
|
||||
<Radio.Group
|
||||
options={optionsAllEnabled}
|
||||
onChange={onChangeAllEnabled}
|
||||
value={optionAllEnable}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
/>
|
||||
<Select value={pageSize.toString()} options={pageSizeOptions}
|
||||
onChange={onChangePageSize} className="select-rows-per-page-en"/>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24}
|
||||
sm={24}
|
||||
md={5}
|
||||
lg={5}
|
||||
xl={5}
|
||||
xxl={5} span={5}>
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
{!showTutorial &&
|
||||
<Button type="primary" onClick={onClickAddNewNSGroup}>Add
|
||||
Nameserver</Button>}
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
{failed &&
|
||||
<Alert message={failed.code} description={failed.message} type="error" showIcon
|
||||
closable/>
|
||||
}
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Table
|
||||
pagination={{
|
||||
pageSize,
|
||||
showSizeChanger: false,
|
||||
showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} users`)
|
||||
}}
|
||||
// className="card-table"
|
||||
className={`access-control-table ${showTutorial ? "card-table card-table-no-placeholder" : "card-table"}`}
|
||||
showSorterTooltip={false}
|
||||
scroll={{x: true}}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={dataTable}>
|
||||
<Column title="Name" dataIndex="name" align="center"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).name.includes(value)}
|
||||
sorter={(a, b) => ((a as any).name.localeCompare((b as any).name))}
|
||||
defaultSortOrder='ascend'
|
||||
render={(text, record) => {
|
||||
return <Button type="text"
|
||||
onClick={() => setUserAndView(record as NameserverGroupDataTable)}
|
||||
className="tooltip-label">{(text && text.trim() !== "") ? text : (record as NameServerGroup).id}</Button>
|
||||
}}
|
||||
/>
|
||||
<Column title="Status" dataIndex="enabled" align="center"
|
||||
render={(text: Boolean) => {
|
||||
return text ? <Tag color="green">enabled</Tag> :
|
||||
<Tag color="red">disabled</Tag>
|
||||
}}
|
||||
/>
|
||||
<Column title="Nameservers" dataIndex="nameservers" align="center"
|
||||
render={(nameservers: NameServer[]) => (
|
||||
<>
|
||||
{nameservers.map(nameserver => (
|
||||
<Tag key={nameserver.ip}>
|
||||
{nameserver.ip}
|
||||
</Tag>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<Column title="All domains" dataIndex="primary" align="center"
|
||||
render={(text: Boolean) => {
|
||||
return text ? <Tag color="blue">yes</Tag> :
|
||||
<Tag>no</Tag>
|
||||
}}
|
||||
/>
|
||||
<Column title="Match domains" dataIndex="domains" align="center"
|
||||
render={(text, record: NameserverGroupDataTable) => {
|
||||
return renderPopoverDomains(text, record.domains, record)
|
||||
}}
|
||||
/>
|
||||
<Column title="Groups" dataIndex="groupsCount" align="center"
|
||||
render={(text, record: NameserverGroupDataTable) => {
|
||||
return renderPopoverGroups(text, record.groups, record)
|
||||
}}
|
||||
/>
|
||||
<Column title="" align="center" width="30px"
|
||||
render={(text, record) => {
|
||||
return (
|
||||
<Dropdown.Button type="text" overlay={actionsMenu}
|
||||
trigger={["click"]}
|
||||
onOpenChange={visible => {
|
||||
if (visible) setNsGroupToAction(record as NameserverGroupDataTable)
|
||||
}}></Dropdown.Button>)
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
{showTutorial &&
|
||||
<Space direction="vertical" size="small" align="center"
|
||||
style={{display: 'flex', padding: '45px 15px', justifyContent: 'center'}}>
|
||||
<Paragraph type="secondary"
|
||||
style={{textAlign: "center", whiteSpace: "pre-line"}}>
|
||||
It looks like you don't have any nameservers. {"\n"}
|
||||
Get started by adding one to your network!
|
||||
</Paragraph>
|
||||
<Button type="primary" onClick={onClickAddNewNSGroup}>Add
|
||||
Nameserver</Button>
|
||||
</Space>
|
||||
}
|
||||
</Card>
|
||||
</Space>
|
||||
<Tabs
|
||||
defaultActiveKey={nsTabKey}
|
||||
items={items}
|
||||
onTabClick={onTabClick}
|
||||
animated={{ inkBar: true, tabPane: false }}
|
||||
tabPosition="top"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
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;
|
||||
@@ -1,21 +1,81 @@
|
||||
import {Input, Select, Space} from 'antd';
|
||||
import React, {useState} from 'react';
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const ExpiresInInput: React.FC<ExpiresInInputProps> = ({value = {}, onChange}) => {
|
||||
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("Days");
|
||||
const [interval, setInterval] = useState("day");
|
||||
|
||||
const triggerChange = (changedValue: { number?: number; interval?: string }) => {
|
||||
onChange?.({number, interval, ...value, ...changedValue});
|
||||
@@ -38,16 +98,16 @@ const ExpiresInInput: React.FC<ExpiresInInputProps> = ({value = {}, onChange}) =
|
||||
type="number"
|
||||
value={value.number || number}
|
||||
onChange={onNumberChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Select style={{width: "100%"}}
|
||||
value={value?.interval || interval}
|
||||
onChange={onIntervalChange}>
|
||||
<Option value="day">Days</Option>
|
||||
<Option value="week">Weeks</Option>
|
||||
<Option value="month">Months</Option>
|
||||
<Option value="year">Years</Option>
|
||||
<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>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
475
src/views/Nameservers.tsx
Normal file
475
src/views/Nameservers.tsx
Normal file
@@ -0,0 +1,475 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as nsGroupActions} from '../store/nameservers';
|
||||
import {Container} from "../components/Container";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Dropdown,
|
||||
Input,
|
||||
Menu,
|
||||
message,
|
||||
Modal,
|
||||
Popover,
|
||||
Radio,
|
||||
RadioChangeEvent,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {filter} from "lodash";
|
||||
import tableSpin from "../components/Spin";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {actions as groupActions} from "../store/group";
|
||||
import {Group} from "../store/group/types";
|
||||
import {TooltipPlacement} from "antd/es/tooltip";
|
||||
import {NameServer, NameServerGroup} from "../store/nameservers/types";
|
||||
import NameServerGroupUpdate from "../components/NameServerGroupUpdate";
|
||||
import {EllipsisOutlined, ExclamationCircleOutlined} from "@ant-design/icons";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
import {usePageSizeHelpers} from "../utils/pageSize";
|
||||
|
||||
const {Title, Paragraph} = Typography;
|
||||
const {Column} = Table;
|
||||
const {confirm} = Modal;
|
||||
|
||||
interface NameserverGroupDataTable extends NameServerGroup {
|
||||
key: string
|
||||
}
|
||||
|
||||
const styleNotification = {marginTop: 85}
|
||||
|
||||
export const Nameservers = () => {
|
||||
const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const {
|
||||
getGroupNamesFromIDs,
|
||||
} = useGetGroupTagHelpers()
|
||||
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const nsGroup = useSelector((state: RootState) => state.nameserverGroup.data);
|
||||
const failed = useSelector((state: RootState) => state.nameserverGroup.failed);
|
||||
const loading = useSelector((state: RootState) => state.nameserverGroup.loading);
|
||||
const updateNameServerGroupVisible = useSelector((state: RootState) => state.nameserverGroup.setupNewNameServerGroupVisible)
|
||||
const savedNSGroup = useSelector((state: RootState) => state.nameserverGroup.savedNameServerGroup)
|
||||
|
||||
const [groupPopupVisible, setGroupPopupVisible] = useState("")
|
||||
const [nsGroupToAction, setNsGroupToAction] = useState(null as NameserverGroupDataTable | null);
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [optionAllEnable, setOptionAllEnable] = useState('enabled');
|
||||
const [dataTable, setDataTable] = useState([] as NameserverGroupDataTable[]);
|
||||
const [showTutorial, setShowTutorial] = useState(false)
|
||||
|
||||
|
||||
const optionsAllEnabled = [{label: 'Enabled', value: 'enabled'}, {label: 'All', value: 'all'}]
|
||||
|
||||
// setUserAndView makes the UserUpdate drawer visible (right side) and sets the user object
|
||||
const setUserAndView = (nsGroup: NameServerGroup) => {
|
||||
dispatch(nsGroupActions.setSetupNewNameServerGroupVisible(true));
|
||||
dispatch(nsGroupActions.setNameServerGroup({
|
||||
id: nsGroup.id,
|
||||
name: nsGroup.name,
|
||||
primary: nsGroup.primary,
|
||||
domains: nsGroup.domains,
|
||||
description: nsGroup.description,
|
||||
nameservers: nsGroup.nameservers,
|
||||
groups: nsGroup.groups,
|
||||
enabled: nsGroup.enabled,
|
||||
} as NameServerGroup));
|
||||
}
|
||||
|
||||
const transformDataTable = (d: NameServerGroup[]): NameserverGroupDataTable[] => {
|
||||
return d.map(p => ({key: p.id, ...p} as NameserverGroupDataTable))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(nsGroupActions.getNameServerGroups.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (nsGroup.length > 0) {
|
||||
setShowTutorial(false)
|
||||
} else {
|
||||
setShowTutorial(true)
|
||||
}
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}, [nsGroup])
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}, [textToSearch, optionAllEnable])
|
||||
|
||||
const filterDataTable = (): NameServerGroup[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let f = filter(nsGroup, (f: NameServerGroup) =>
|
||||
((f.name).toLowerCase().includes(t) ||
|
||||
f.name.includes(t) || t === "" ||
|
||||
getGroupNamesFromIDs(f.groups).find(u => u.toLowerCase().trim().includes(t)) ||
|
||||
f.domains.find(d => d.toLowerCase().trim().includes(t)) ||
|
||||
f.nameservers.find(n => n.ip.includes(t)))
|
||||
) as NameServerGroup[]
|
||||
if (optionAllEnable !== "all") {
|
||||
f = filter(f, (f) => f.enabled)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
const onChangeAllEnabled = ({target: {value}}: RadioChangeEvent) => {
|
||||
setOptionAllEnable(value)
|
||||
}
|
||||
|
||||
const onChangeTextToSearch = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setTextToSearch(e.target.value)
|
||||
};
|
||||
|
||||
const searchDataTable = () => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}
|
||||
|
||||
const onClickEdit = () => {
|
||||
dispatch(nsGroupActions.setSetupNewNameServerGroupVisible(true));
|
||||
dispatch(nsGroupActions.setNameServerGroup({
|
||||
id: nsGroupToAction?.id,
|
||||
name: nsGroupToAction?.name,
|
||||
primary: nsGroupToAction?.primary,
|
||||
domains: nsGroupToAction?.domains,
|
||||
description: nsGroupToAction?.description,
|
||||
groups: nsGroupToAction?.groups,
|
||||
enabled: nsGroupToAction?.enabled,
|
||||
nameservers: nsGroupToAction?.nameservers,
|
||||
} as NameServerGroup));
|
||||
}
|
||||
|
||||
const showConfirmDelete = () => {
|
||||
let name = nsGroupToAction ? nsGroupToAction.name : '';
|
||||
confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
title: "Delete Nameserver group \"" + name + "\"",
|
||||
width: 600,
|
||||
content: <Space direction="vertical" size="small">
|
||||
<Paragraph>Are you sure you want to delete this nameserver group from your account?</Paragraph>
|
||||
</Space>,
|
||||
okType: 'danger',
|
||||
onOk() {
|
||||
dispatch(nsGroupActions.deleteNameServerGroup.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: nsGroupToAction?.id || ''
|
||||
}));
|
||||
},
|
||||
onCancel() {
|
||||
setNsGroupToAction(null);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const renderPopoverGroups = (label: string, rowGroups: string[] | null, userToAction: NameserverGroupDataTable) => {
|
||||
|
||||
let groupsMap = new Map<string, Group>();
|
||||
groups.forEach(g => {
|
||||
groupsMap.set(g.id!, g)
|
||||
})
|
||||
|
||||
let displayGroups: Group[] = []
|
||||
if (rowGroups) {
|
||||
displayGroups = rowGroups.filter(g => groupsMap.get(g)).map(g => groupsMap.get(g)!)
|
||||
}
|
||||
|
||||
let btn = <Button type="link" onClick={() => setUserAndView(userToAction)}>{displayGroups.length}</Button>
|
||||
if (!displayGroups || displayGroups!.length < 1) {
|
||||
return btn
|
||||
}
|
||||
|
||||
const content = displayGroups?.map((g, i) => {
|
||||
const _g = g as Group
|
||||
const peersCount = ` - ${_g.peers_count || 0} ${(!_g.peers_count || parseInt(_g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<div key={i}>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{_g.name}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
const mainContent = (<Space direction="vertical">{content}</Space>)
|
||||
let popoverPlacement = "top"
|
||||
if (content && content.length > 5) {
|
||||
popoverPlacement = "rightTop"
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover placement={popoverPlacement as TooltipPlacement}
|
||||
key={userToAction.id}
|
||||
onOpenChange={(b: boolean) => onPopoverVisibleChange(b, userToAction.key+"group")}
|
||||
open={groupPopupVisible === userToAction.key+"group"}
|
||||
content={mainContent}
|
||||
title={null}>
|
||||
{btn}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
const renderPopoverDomains = (_: string, inputDomains: string[] | null, userToAction: NameserverGroupDataTable) => {
|
||||
var domains = [] as string[]
|
||||
if (inputDomains?.length) {
|
||||
domains = inputDomains
|
||||
}
|
||||
|
||||
let btn = <Button type="link"
|
||||
onClick={() => setUserAndView(userToAction)}>{domains.length ? domains.length : 0}</Button>
|
||||
if (!domains || domains!.length < 1) {
|
||||
return btn
|
||||
}
|
||||
|
||||
const content = domains?.map((d, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{d}</strong>
|
||||
</Tag>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const mainContent = (<Space direction="vertical">{content}</Space>)
|
||||
let popoverPlacement = "top"
|
||||
if (content && content.length > 5) {
|
||||
popoverPlacement = "rightTop"
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover placement={popoverPlacement as TooltipPlacement}
|
||||
key={userToAction.id}
|
||||
onOpenChange={(b: boolean) => onPopoverVisibleChange(b, userToAction.key+"domain")}
|
||||
open={groupPopupVisible === userToAction.key+"domain"}
|
||||
content={mainContent}
|
||||
title={null}>
|
||||
{btn}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (updateNameServerGroupVisible) {
|
||||
setGroupPopupVisible("")
|
||||
}
|
||||
}, [updateNameServerGroupVisible])
|
||||
|
||||
const createKey = 'saving';
|
||||
useEffect(() => {
|
||||
if (savedNSGroup.loading) {
|
||||
message.loading({content: 'Saving...', key: createKey, duration: 0, style: styleNotification});
|
||||
} else if (savedNSGroup.success) {
|
||||
message.success({
|
||||
content: 'Nameserver has been successfully saved.',
|
||||
key: createKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(nsGroupActions.setSetupNewNameServerGroupVisible(false));
|
||||
dispatch(nsGroupActions.setSavedNameServerGroup({...savedNSGroup, success: false}));
|
||||
dispatch(nsGroupActions.resetSavedNameServerGroup(null))
|
||||
} else if (savedNSGroup.error) {
|
||||
let errorMsg = "Failed to update nameserver group"
|
||||
switch (savedNSGroup.error.statusCode) {
|
||||
case 403:
|
||||
errorMsg = "Failed to update nameserver group. You might not have enough permissions."
|
||||
break
|
||||
default:
|
||||
errorMsg = savedNSGroup.error.data.message ? savedNSGroup.error.data.message : errorMsg
|
||||
break
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: createKey,
|
||||
duration: 5,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(nsGroupActions.setSavedNameServerGroup({...savedNSGroup, error: null}));
|
||||
dispatch(nsGroupActions.resetSavedNameServerGroup(null))
|
||||
}
|
||||
}, [savedNSGroup])
|
||||
|
||||
const onPopoverVisibleChange = (b:boolean, key: string) => {
|
||||
if (updateNameServerGroupVisible) {
|
||||
setGroupPopupVisible("")
|
||||
} else {
|
||||
if (b) {
|
||||
setGroupPopupVisible(key)
|
||||
} else {
|
||||
setGroupPopupVisible("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const itemsMenuAction = [
|
||||
{
|
||||
key: "edit",
|
||||
label: (<Button type="text" onClick={() => onClickEdit()}>View</Button>)
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
label: (<Button type="text" onClick={() => showConfirmDelete()}>Delete</Button>)
|
||||
},
|
||||
]
|
||||
|
||||
const actionsMenu = (<Menu items={itemsMenuAction}></Menu>)
|
||||
|
||||
const onClickAddNewNSGroup = () => {
|
||||
dispatch(nsGroupActions.setSetupNewNameServerGroupVisible(true));
|
||||
dispatch(nsGroupActions.setNameServerGroup({
|
||||
enabled: true,
|
||||
primary: true,
|
||||
} as NameServerGroup))
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paragraph>Add nameservers for domain name resolution in your NetBird network</Paragraph>
|
||||
<Space direction="vertical" size="large" style={{display: 'flex'}}>
|
||||
<Row gutter={[16, 24]}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
|
||||
<Input allowClear value={textToSearch} onPressEnter={searchDataTable}
|
||||
placeholder="Search..." onChange={onChangeTextToSearch}/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
|
||||
<Space size="middle">
|
||||
<Radio.Group
|
||||
options={optionsAllEnabled}
|
||||
onChange={onChangeAllEnabled}
|
||||
value={optionAllEnable}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
/>
|
||||
<Select value={pageSize.toString()} options={pageSizeOptions}
|
||||
onChange={onChangePageSize} className="select-rows-per-page-en"/>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col xs={24}
|
||||
sm={24}
|
||||
md={5}
|
||||
lg={5}
|
||||
xl={5}
|
||||
xxl={5} span={5}>
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
{!showTutorial &&
|
||||
<Button type="primary" onClick={onClickAddNewNSGroup}>Add
|
||||
Nameserver</Button>}
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
{failed &&
|
||||
<Alert message={failed.code} description={failed.message} type="error" showIcon
|
||||
closable/>
|
||||
}
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Table
|
||||
pagination={{
|
||||
pageSize,
|
||||
showSizeChanger: false,
|
||||
showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} nameservers`)
|
||||
}}
|
||||
// className="card-table"
|
||||
className={`access-control-table ${showTutorial ? "card-table card-table-no-placeholder" : "card-table"}`}
|
||||
showSorterTooltip={false}
|
||||
scroll={{x: true}}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={dataTable}>
|
||||
<Column title="Name" dataIndex="name" align="center"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).name.includes(value)}
|
||||
sorter={(a, b) => ((a as any).name.localeCompare((b as any).name))}
|
||||
defaultSortOrder='ascend'
|
||||
render={(text, record) => {
|
||||
return <Button type="text"
|
||||
onClick={() => setUserAndView(record as NameserverGroupDataTable)}
|
||||
className="tooltip-label">{(text && text.trim() !== "") ? text : (record as NameServerGroup).id}</Button>
|
||||
}}
|
||||
/>
|
||||
<Column title="Status" dataIndex="enabled" align="center"
|
||||
render={(text: Boolean) => {
|
||||
return text ? <Tag color="green">enabled</Tag> :
|
||||
<Tag color="red">disabled</Tag>
|
||||
}}
|
||||
/>
|
||||
<Column title="Nameservers" dataIndex="nameservers" align="center"
|
||||
render={(nameservers: NameServer[]) => (
|
||||
<>
|
||||
{nameservers.map(nameserver => (
|
||||
<Tag key={nameserver.ip}>
|
||||
{nameserver.ip}
|
||||
</Tag>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<Column title="All domains" dataIndex="primary" align="center"
|
||||
render={(text: Boolean) => {
|
||||
return text ? <Tag color="blue">yes</Tag> :
|
||||
<Tag>no</Tag>
|
||||
}}
|
||||
/>
|
||||
<Column title="Match domains" dataIndex="domains" align="center"
|
||||
render={(text, record: NameserverGroupDataTable) => {
|
||||
return renderPopoverDomains(text, record.domains, record)
|
||||
}}
|
||||
/>
|
||||
<Column title="Groups" dataIndex="groupsCount" align="center"
|
||||
render={(text, record: NameserverGroupDataTable) => {
|
||||
return renderPopoverGroups(text, record.groups, record)
|
||||
}}
|
||||
/>
|
||||
<Column title="" align="center" width="30px"
|
||||
render={(text, record) => {
|
||||
return (
|
||||
<Dropdown trigger={["click"]} overlay={actionsMenu} onOpenChange={visible => {
|
||||
if (visible) setNsGroupToAction(record as NameserverGroupDataTable)
|
||||
}}>
|
||||
<Button type="text">
|
||||
<Space>
|
||||
<EllipsisOutlined />
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
{showTutorial &&
|
||||
<Space direction="vertical" size="small" align="center"
|
||||
style={{display: 'flex', padding: '45px 15px', justifyContent: 'center'}}>
|
||||
<Paragraph type="secondary"
|
||||
style={{textAlign: "center", whiteSpace: "pre-line"}}>
|
||||
It looks like you don't have any nameservers. {"\n"}
|
||||
Get started by adding one to your network!
|
||||
</Paragraph>
|
||||
<Button type="primary" onClick={onClickAddNewNSGroup}>Add
|
||||
Nameserver</Button>
|
||||
</Space>
|
||||
}
|
||||
</Card>
|
||||
</Space>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Nameservers;
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {capitalize, formatOS, timeAgo} from "../utils/common";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as peerActions} from '../store/peer';
|
||||
import {actions as groupActions} from '../store/group';
|
||||
import {actions as routeActions} from '../store/route';
|
||||
import {Container} from "../components/Container";
|
||||
import {EllipsisOutlined, ExclamationCircleOutlined, ReloadOutlined,} from '@ant-design/icons';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -31,15 +32,17 @@ import {
|
||||
} from "antd";
|
||||
import {Peer, PeerDataTable} from "../store/peer/types";
|
||||
import {filter} from "lodash"
|
||||
import {formatOS, timeAgo} from "../utils/common";
|
||||
import {ExclamationCircleOutlined} from "@ant-design/icons";
|
||||
import {Group, GroupPeer} from "../store/group/types";
|
||||
import PeerUpdate from "../components/PeerUpdate";
|
||||
import tableSpin from "../components/Spin";
|
||||
import {TooltipPlacement} from "antd/es/tooltip";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {actions as userActions} from "../store/user";
|
||||
import ButtonCopyMessage from "../components/ButtonCopyMessage";
|
||||
import {usePageSizeHelpers} from "../utils/pageSize";
|
||||
import AddPeerPopup from "../components/popups/addpeer/addpeer/AddPeerPopup";
|
||||
import {getLocalItem, setLocalItem, StorageKey} from "../services/local";
|
||||
import {useOidcUser} from "@axa-fr/react-oidc";
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
const {Column} = Table;
|
||||
@@ -47,8 +50,9 @@ const {confirm} = Modal;
|
||||
|
||||
export const Peers = () => {
|
||||
|
||||
//const {accessToken} = useOidcAccessToken()
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const {onChangePageSize, pageSizeOptions, pageSize} = usePageSizeHelpers()
|
||||
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const peers = useSelector((state: RootState) => state.peer.data);
|
||||
@@ -62,20 +66,18 @@ export const Peers = () => {
|
||||
const updatedPeer = useSelector((state: RootState) => state.peer.updatedPeer);
|
||||
const updateGroupsVisible = useSelector((state: RootState) => state.peer.updateGroupsVisible)
|
||||
const users = useSelector((state: RootState) => state.user.data);
|
||||
const [addPeerModalOpen, setAddPeerModalOpen] = useState(false);
|
||||
const {oidcUser} = useOidcUser();
|
||||
|
||||
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [optionOnOff, setOptionOnOff] = useState('all');
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [dataTable, setDataTable] = useState([] as PeerDataTable[]);
|
||||
const [peerToAction, setPeerToAction] = useState(null as PeerDataTable | null);
|
||||
const [groupPopupVisible, setGroupPopupVisible] = useState(false as boolean | undefined)
|
||||
const [groupPopupVisible, setGroupPopupVisible] = useState("")
|
||||
const [showTutorial, setShowTutorial] = useState(false)
|
||||
|
||||
const pageSizeOptions = [
|
||||
{label: "5", value: "5"},
|
||||
{label: "10", value: "10"},
|
||||
{label: "15", value: "15"}
|
||||
]
|
||||
const [hadFirstRun, setHadFirstRun] = useState(true)
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
|
||||
const optionsOnOff = [{label: 'Online', value: 'on'}, {label: 'All', value: 'all'}]
|
||||
|
||||
@@ -92,7 +94,6 @@ export const Peers = () => {
|
||||
const actionsMenu = (<Menu items={itemsMenuAction}></Menu>)
|
||||
|
||||
const transformDataTable = (d: Peer[]): PeerDataTable[] => {
|
||||
const peer_ids = d.map(_p => _p.id)
|
||||
return d.map((p) => {
|
||||
const gs = groups
|
||||
.filter(g => g.peers?.find((_p: GroupPeer) => _p.id === p.id))
|
||||
@@ -106,16 +107,38 @@ export const Peers = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const isUserAdmin = (userId: string): boolean => {
|
||||
return users.find(u => u.id === userId)?.role === "admin"
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
dispatch(userActions.getUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
dispatch(peerActions.getPeers.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
if(oidcUser && isUserAdmin(oidcUser.sub))
|
||||
dispatch(routeActions.getRoutes.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(userActions.getUsers.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
dispatch(peerActions.getPeers.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
dispatch(routeActions.getRoutes.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
getLocalItem<boolean>(StorageKey.hadFirstRun).then(f => setHadFirstRun(f === null? false : f))
|
||||
refresh()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!hadFirstRun) {
|
||||
setLocalItem(StorageKey.hadFirstRun, true).then()
|
||||
setAddPeerModalOpen(true)
|
||||
} else {
|
||||
setAddPeerModalOpen(false)
|
||||
}
|
||||
}, [hadFirstRun])
|
||||
|
||||
useEffect(() => {
|
||||
if (peers.length) {
|
||||
setShowTutorial(false)
|
||||
if (!hadFirstRun) {
|
||||
setHadFirstRun(true)
|
||||
}
|
||||
} else {
|
||||
setShowTutorial(true)
|
||||
}
|
||||
@@ -180,10 +203,11 @@ export const Peers = () => {
|
||||
dispatch(peerActions.setUpdatedPeer({...updatedPeer, success: false}))
|
||||
dispatch(peerActions.resetUpdatedPeer(null))
|
||||
} else if (updatedPeer.error) {
|
||||
let msg = updatedPeer.error.data ? capitalize(updatedPeer.error.data.message) : updatedPeer.error.message
|
||||
message.error({
|
||||
content: 'Failed to update peer. You might not have enough permissions.',
|
||||
content: msg,
|
||||
key: updatePeerKey,
|
||||
duration: 2,
|
||||
duration: 3,
|
||||
style
|
||||
});
|
||||
dispatch(peerActions.setUpdatedPeer({...updatedPeer, error: null}))
|
||||
@@ -229,15 +253,10 @@ export const Peers = () => {
|
||||
setOptionOnOff(value)
|
||||
}
|
||||
|
||||
const onChangePageSize = (value: string) => {
|
||||
setPageSize(parseInt(value.toString()))
|
||||
}
|
||||
|
||||
|
||||
const showConfirmDelete = () => {
|
||||
let peerRoutes: string[] = []
|
||||
routes.forEach((r) => {
|
||||
if (r.peer == peerToAction?.ip) {
|
||||
if (r.peer == peerToAction?.id) {
|
||||
peerRoutes.push(r.network_id)
|
||||
}
|
||||
})
|
||||
@@ -279,16 +298,15 @@ export const Peers = () => {
|
||||
</div>
|
||||
}
|
||||
let name = peerToAction ? peerToAction.name : ''
|
||||
confirm({
|
||||
confirmModal.confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
title: "Delete peer \"" + name + "\"",
|
||||
width: 600,
|
||||
content: contentModule,
|
||||
okType: 'danger',
|
||||
onOk() {
|
||||
dispatch(peerActions.deletedPeer.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: peerToAction ? peerToAction.ip : ''
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: (peerToAction && peerToAction.id) ? peerToAction.id! : ""
|
||||
}));
|
||||
},
|
||||
onCancel() {
|
||||
@@ -298,12 +316,11 @@ export const Peers = () => {
|
||||
}
|
||||
|
||||
const showConfirmEnableSSH = (record: PeerDataTable) => {
|
||||
confirm({
|
||||
confirmModal.confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
title: "Enable SSH Server for \"" + record.name + "\"?",
|
||||
width: 600,
|
||||
content: "Experimental feature. Enabling this option allows remote SSH access to this machine from other connected network participants.",
|
||||
okType: 'danger',
|
||||
onOk() {
|
||||
handleSwitchSSH(record, true)
|
||||
},
|
||||
@@ -318,7 +335,7 @@ export const Peers = () => {
|
||||
ssh_enabled: checked,
|
||||
name: record.name
|
||||
} as Peer
|
||||
dispatch(peerActions.updatePeer.request({getAccessTokenSilently: getAccessTokenSilently, payload: peer}));
|
||||
dispatch(peerActions.updatePeer.request({getAccessTokenSilently: getTokenSilently, payload: peer}));
|
||||
|
||||
}
|
||||
|
||||
@@ -329,15 +346,19 @@ export const Peers = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (updateGroupsVisible) {
|
||||
setGroupPopupVisible(false)
|
||||
setGroupPopupVisible("")
|
||||
}
|
||||
}, [updateGroupsVisible])
|
||||
|
||||
const onPopoverVisibleChange = (b: boolean) => {
|
||||
const onPopoverVisibleChange = (b: boolean, key: string) => {
|
||||
if (updateGroupsVisible) {
|
||||
setGroupPopupVisible(false)
|
||||
setGroupPopupVisible("")
|
||||
} else {
|
||||
setGroupPopupVisible(undefined)
|
||||
if(b) {
|
||||
setGroupPopupVisible(key)
|
||||
} else {
|
||||
setGroupPopupVisible("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,6 +388,12 @@ export const Peers = () => {
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
let btn = <Button type="link" onClick={() => setUpdateGroupsVisible(peerToAction, true)}>{label}</Button>
|
||||
if (!content || content!.length < 1) {
|
||||
return btn
|
||||
}
|
||||
|
||||
const mainContent = (<Space direction="vertical">{content}</Space>)
|
||||
let popoverPlacement = "top"
|
||||
if (content && content.length > 5) {
|
||||
@@ -375,7 +402,7 @@ export const Peers = () => {
|
||||
|
||||
return (
|
||||
<Popover placement={popoverPlacement as TooltipPlacement} key={peerToAction.key} content={mainContent}
|
||||
onOpenChange={onPopoverVisibleChange} open={groupPopupVisible}
|
||||
onOpenChange={(b:boolean) => onPopoverVisibleChange(b, peerToAction.key)} open={groupPopupVisible === peerToAction.key}
|
||||
title={null}>
|
||||
<Button type="link" onClick={() => setUpdateGroupsVisible(peerToAction, true)}>{label}</Button>
|
||||
</Popover>
|
||||
@@ -391,7 +418,7 @@ export const Peers = () => {
|
||||
styleNotification={{}}/>
|
||||
}
|
||||
|
||||
const body = <span style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}>
|
||||
const body = <span style={{textAlign: "left"}}>
|
||||
<Row>
|
||||
<ButtonCopyMessage keyMessage={peer.dns_label}
|
||||
toCopy={peer.dns_label}
|
||||
@@ -405,7 +432,7 @@ export const Peers = () => {
|
||||
toCopy={peer.ip}
|
||||
body={<Text type="secondary">{peer.ip}</Text>}
|
||||
messageText={'Peer IP copied'}
|
||||
style={{marginTop:'-10px'}}
|
||||
style={{marginTop: '-10px'}}
|
||||
styleNotification={{}}/>
|
||||
</Row>
|
||||
</span>
|
||||
@@ -416,17 +443,23 @@ export const Peers = () => {
|
||||
|
||||
const renderName = (peer: PeerDataTable) => {
|
||||
const userEmail = users?.find(u => u.id === peer.user_id)?.email
|
||||
let expiry =!peer.login_expiration_enabled ? <div><Tag><Text type="secondary" style={{fontSize: 10}}>expiration disabled</Text></Tag></div> : null
|
||||
if (!userEmail) {
|
||||
return <Button type="text" onClick={() => setUpdateGroupsVisible(peer, true)}>
|
||||
<Text strong>{peer.name}</Text>
|
||||
return <Button type="text" style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}
|
||||
onClick={() => setUpdateGroupsVisible(peer, true)}>
|
||||
<span style={{textAlign: "left"}}>
|
||||
<Row><Text strong>{peer.name}</Text></Row>
|
||||
</span>
|
||||
</Button>
|
||||
}
|
||||
return <div>
|
||||
<Button type="text" style={{height: "auto", whiteSpace: "normal", textAlign: "left"}}
|
||||
<Button type="text"
|
||||
onClick={() => setUpdateGroupsVisible(peer, true)}>
|
||||
<Text strong>{peer.name}</Text>
|
||||
<br/>
|
||||
<Text type="secondary">{userEmail}</Text>
|
||||
<span style={{textAlign: "left"}}>
|
||||
<Row> <Text strong>{peer.name}</Text></Row>
|
||||
<Row><Text type="secondary">{userEmail}</Text></Row>
|
||||
<Row> {expiry}</Row>
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
@@ -437,8 +470,10 @@ export const Peers = () => {
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Peers</Title>
|
||||
<Paragraph>A list of all the machines in your account including their name, IP and
|
||||
status.</Paragraph>
|
||||
{showTutorial && <Paragraph type={"secondary"}>A list of all the machines in your account including their name, IP and
|
||||
status.</Paragraph>}
|
||||
{!showTutorial && <Paragraph>A list of all the machines in your account including their name, IP and
|
||||
status.</Paragraph>}
|
||||
<Space direction="vertical" size="large" style={{display: 'flex'}}>
|
||||
<Row gutter={[16, 24]}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
|
||||
@@ -453,8 +488,10 @@ export const Peers = () => {
|
||||
value={optionOnOff}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
disabled={showTutorial}
|
||||
/>
|
||||
<Select value={pageSize.toString()} options={pageSizeOptions}
|
||||
disabled={showTutorial}
|
||||
onChange={onChangePageSize} className="select-rows-per-page-en"/>
|
||||
</Space>
|
||||
</Col>
|
||||
@@ -466,9 +503,7 @@ export const Peers = () => {
|
||||
xxl={5} span={5}>
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
{!showTutorial &&
|
||||
<Link to="/add-peer" className="ant-btn ant-btn-primary ant-btn-block">Add
|
||||
Peer</Link>}
|
||||
{!showTutorial && <Button type="primary" onClick={() => setAddPeerModalOpen(true)}>Add peer</Button>}
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
@@ -479,7 +514,7 @@ export const Peers = () => {
|
||||
closable/>
|
||||
}
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Table
|
||||
{!showTutorial && (<Table
|
||||
pagination={{
|
||||
pageSize,
|
||||
showSizeChanger: false,
|
||||
@@ -512,9 +547,20 @@ export const Peers = () => {
|
||||
}}
|
||||
/>
|
||||
<Column title="Status" dataIndex="connected" align="center"
|
||||
render={(text, record, index) => {
|
||||
return text ? <Tag color="green">online</Tag> :
|
||||
render={(text, record: PeerDataTable, index) => {
|
||||
|
||||
let status = text ? <Tag color="green">online</Tag> :
|
||||
<Tag color="red">offline</Tag>
|
||||
|
||||
if (record.login_expired) {
|
||||
return <Tooltip
|
||||
title="The peer is offline and needs to be re-authenticated because its login has expired ">
|
||||
<Tag color="orange">needs login</Tag>
|
||||
</Tooltip>
|
||||
|
||||
}
|
||||
|
||||
return status
|
||||
}}
|
||||
/>
|
||||
<Column title="Groups" dataIndex="groupsCount" align="center"
|
||||
@@ -551,7 +597,10 @@ export const Peers = () => {
|
||||
|
||||
<Column title="LastSeen" dataIndex="last_seen"
|
||||
render={(text, record, index) => {
|
||||
return (record as PeerDataTable).connected ? 'just now' : timeAgo(text)
|
||||
let dt = new Date(text)
|
||||
return <Popover content={dt.toLocaleString()}>
|
||||
{(record as PeerDataTable).connected ? 'just now' : timeAgo(text)}
|
||||
</Popover>
|
||||
}}
|
||||
/>
|
||||
<Column title="OS" dataIndex="os"
|
||||
@@ -562,24 +611,36 @@ export const Peers = () => {
|
||||
<Column title="Version" dataIndex="version"/>
|
||||
<Column title="" align="center"
|
||||
render={(text, record, index) => {
|
||||
return <Dropdown.Button type="text" overlay={actionsMenu}
|
||||
trigger={["click"]}
|
||||
onOpenChange={visible => {
|
||||
if (visible) setPeerToAction(record as PeerDataTable)
|
||||
}}></Dropdown.Button>
|
||||
return (
|
||||
<Dropdown trigger={["click"]} overlay={actionsMenu} onOpenChange={visible => {
|
||||
if (visible) setPeerToAction(record as PeerDataTable)
|
||||
}}>
|
||||
<Button type="text">
|
||||
<Space>
|
||||
<EllipsisOutlined />
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
</Table>)}
|
||||
{showTutorial &&
|
||||
<Space direction="vertical" size="small" align="center"
|
||||
style={{display: 'flex', padding: '45px 15px', justifyContent: 'center'}}>
|
||||
<Paragraph type="secondary"
|
||||
<Title level={4}
|
||||
style={{textAlign: "center"}}>
|
||||
Get Started
|
||||
</Title>
|
||||
<Paragraph
|
||||
style={{textAlign: "center", whiteSpace: "pre-line"}}>
|
||||
It looks like you don't have any connected machines. {"\n"}
|
||||
Get started by adding one to your network!
|
||||
Get started by adding one to your network.
|
||||
</Paragraph>
|
||||
<Link to="/add-peer" className="ant-btn ant-btn-primary ant-btn-block">Add
|
||||
Peer</Link>
|
||||
<Button size={"middle"} type="primary" onClick={() => setAddPeerModalOpen(true)}>
|
||||
Add new peer
|
||||
</Button>
|
||||
|
||||
</Space>
|
||||
}
|
||||
</Card>
|
||||
@@ -588,6 +649,19 @@ export const Peers = () => {
|
||||
</Row>
|
||||
</Container>
|
||||
<PeerUpdate/>
|
||||
<Modal
|
||||
open={addPeerModalOpen}
|
||||
onOk={() => setAddPeerModalOpen(false)}
|
||||
onCancel={() => {
|
||||
setAddPeerModalOpen(false)
|
||||
setHadFirstRun(true)
|
||||
}}
|
||||
footer={[]}
|
||||
width={780}
|
||||
>
|
||||
<AddPeerPopup greeting={!hadFirstRun ? "Hi there!" : ""} headline={!hadFirstRun ? "It's time to add your first device." : "Add new peer"}/>
|
||||
</Modal>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
436
src/views/RegularUsers.tsx
Normal file
436
src/views/RegularUsers.tsx
Normal file
@@ -0,0 +1,436 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as userActions} from '../store/user';
|
||||
import {Container} from "../components/Container";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Dropdown, Empty,
|
||||
Input,
|
||||
Menu,
|
||||
message, Modal,
|
||||
Popover,
|
||||
Row,
|
||||
Select,
|
||||
Space, Switch,
|
||||
Table,
|
||||
Tag, Tooltip,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {User, UserToSave} from "../store/user/types";
|
||||
import {filter} from "lodash";
|
||||
import tableSpin from "../components/Spin";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {actions as groupActions} from "../store/group";
|
||||
import {Group} from "../store/group/types";
|
||||
import {TooltipPlacement} from "antd/es/tooltip";
|
||||
import {capitalize, isLocalDev, isNetBirdHosted} from "../utils/common";
|
||||
import {usePageSizeHelpers} from "../utils/pageSize";
|
||||
import AddServiceUserPopup from "../components/popups/AddServiceUserPopup";
|
||||
import InviteUserPopup from "../components/popups/InviteUserPopup";
|
||||
import {Peer, PeerDataTable} from "../store/peer/types";
|
||||
import {ExclamationCircleOutlined, MinusOutlined} from "@ant-design/icons";
|
||||
import {actions as peerActions} from "../store/peer";
|
||||
import {useOidcUser} from "@axa-fr/react-oidc";
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
const {Column} = Table;
|
||||
|
||||
interface UserDataTable extends User {
|
||||
key: string
|
||||
}
|
||||
|
||||
const styleNotification = {marginTop: 85}
|
||||
|
||||
export const RegularUsers = () => {
|
||||
const {onChangePageSize, pageSizeOptions, pageSize} = usePageSizeHelpers()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const users = useSelector((state: RootState) => state.user.regularUsers);
|
||||
const failed = useSelector((state: RootState) => state.user.failed);
|
||||
const loading = useSelector((state: RootState) => state.user.loading);
|
||||
const updateUserDrawerVisible = useSelector((state: RootState) => state.user.updateUserDrawerVisible)
|
||||
const savedUser = useSelector((state: RootState) => state.user.savedUser)
|
||||
|
||||
const [groupPopupVisible, setGroupPopupVisible] = useState("");
|
||||
const [userToAction, setUserToAction] = useState(null as UserDataTable | null);
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [dataTable, setDataTable] = useState([] as UserDataTable[]);
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
|
||||
// setUserAndView makes the UserUpdate drawer visible (right side) and sets the user object
|
||||
const setUserAndView = (user: User) => {
|
||||
dispatch(userActions.setUpdateUserDrawerVisible(true));
|
||||
dispatch(userActions.setUser({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
auto_groups: user.auto_groups ? user.auto_groups : [],
|
||||
name: user.name,
|
||||
is_current: user.is_current
|
||||
} as User));
|
||||
}
|
||||
|
||||
const transformDataTable = (d: User[]): UserDataTable[] => {
|
||||
return d.map(p => ({key: p.id, ...p} as UserDataTable))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(userActions.getRegularUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
}, [savedUser])
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(users))
|
||||
}, [users])
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}, [textToSearch])
|
||||
|
||||
const filterDataTable = (): User[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let f: User[] = filter(users, (f: User) =>
|
||||
((f.email || f.id).toLowerCase().includes(t) || f.name.toLowerCase().includes(t) || f.role.includes(t) || t === "")
|
||||
) as User[]
|
||||
return f
|
||||
}
|
||||
|
||||
const onChangeTextToSearch = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setTextToSearch(e.target.value)
|
||||
};
|
||||
|
||||
const searchDataTable = () => {
|
||||
const data = filterDataTable()
|
||||
setDataTable(transformDataTable(data))
|
||||
}
|
||||
|
||||
const onClickEdit = () => {
|
||||
dispatch(userActions.setUpdateUserDrawerVisible(true));
|
||||
dispatch(userActions.setUser({
|
||||
id: userToAction?.id,
|
||||
email: userToAction?.email,
|
||||
auto_groups: userToAction?.auto_groups ? userToAction?.auto_groups : [],
|
||||
name: userToAction?.name,
|
||||
role: userToAction?.role,
|
||||
is_blocked: userToAction?.is_blocked,
|
||||
} as User));
|
||||
}
|
||||
|
||||
const onClickInviteUser = () => {
|
||||
dispatch(userActions.setInviteUserPopupVisible(true));
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if(users) {
|
||||
let currentUser = users.find((user) => user.is_current)
|
||||
if(currentUser) {
|
||||
setIsAdmin(currentUser.role === "admin");
|
||||
}
|
||||
}
|
||||
}, [users])
|
||||
|
||||
const renderPopoverGroups = (label: string, rowGroups: string[] | string[] | null, userToAction: UserDataTable) => {
|
||||
|
||||
let groupsMap = new Map<string, Group>();
|
||||
groups.forEach(g => {
|
||||
groupsMap.set(g.id!, g)
|
||||
})
|
||||
|
||||
let displayGroups: Group[] = []
|
||||
if (rowGroups) {
|
||||
displayGroups = rowGroups.filter(g => groupsMap.get(g)).map(g => groupsMap.get(g)!)
|
||||
}
|
||||
|
||||
let btn = <Button type="link" onClick={() => setUserAndView(userToAction)}>{displayGroups.length}</Button>
|
||||
if (!displayGroups || displayGroups!.length < 1) {
|
||||
return btn
|
||||
}
|
||||
|
||||
const content = displayGroups?.map((g, i) => {
|
||||
const _g = g as Group
|
||||
const peersCount = ` - ${_g.peers_count || 0} ${(!_g.peers_count || parseInt(_g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<div key={i}>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{_g.name}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
const mainContent = (<Space direction="vertical">{content}</Space>)
|
||||
let popoverPlacement = "top"
|
||||
if (content && content.length > 5) {
|
||||
popoverPlacement = "rightTop"
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover placement={popoverPlacement as TooltipPlacement}
|
||||
key={userToAction.id}
|
||||
onOpenChange={(b: boolean) => onPopoverVisibleChange(b, userToAction.key)}
|
||||
open={groupPopupVisible === userToAction.key}
|
||||
content={mainContent}
|
||||
title={null}>
|
||||
{btn}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (updateUserDrawerVisible) {
|
||||
setGroupPopupVisible("")
|
||||
}
|
||||
}, [updateUserDrawerVisible])
|
||||
|
||||
const createKey = 'saving';
|
||||
useEffect(() => {
|
||||
if (savedUser.loading) {
|
||||
message.loading({content: 'Saving...', key: createKey, duration: 0, style: styleNotification});
|
||||
} else if (savedUser.success) {
|
||||
message.success({
|
||||
content: 'User has been successfully saved.',
|
||||
key: createKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(userActions.setUpdateUserDrawerVisible(false));
|
||||
dispatch(userActions.setSavedUser({...savedUser, success: false}));
|
||||
dispatch(userActions.resetSavedUser(null))
|
||||
} else if (savedUser.error) {
|
||||
let errorMsg = "Failed to update user"
|
||||
switch (savedUser.error.statusCode) {
|
||||
case 412:
|
||||
case 403:
|
||||
if (savedUser.error.data) {
|
||||
errorMsg = capitalize(savedUser.error.data.message)
|
||||
}
|
||||
break
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: createKey,
|
||||
duration: 5,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(userActions.setSavedUser({...savedUser, error: null}));
|
||||
dispatch(userActions.resetSavedUser(null))
|
||||
}
|
||||
}, [savedUser])
|
||||
|
||||
const onPopoverVisibleChange = (b: boolean, key: string) => {
|
||||
if (updateUserDrawerVisible) {
|
||||
setGroupPopupVisible("")
|
||||
} else {
|
||||
if (b) {
|
||||
setGroupPopupVisible(key)
|
||||
} else {
|
||||
setGroupPopupVisible("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const itemsMenuAction = [
|
||||
{
|
||||
key: "edit",
|
||||
label: (<Button type="text" onClick={() => onClickEdit()}>View</Button>)
|
||||
},
|
||||
|
||||
]
|
||||
const actionsMenu = (<Menu items={itemsMenuAction}></Menu>)
|
||||
|
||||
const handleEditUser = (user: UserDataTable) => {
|
||||
dispatch(userActions.setUser({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
auto_groups: user.auto_groups ? user.auto_groups : [],
|
||||
name: user.name,
|
||||
is_current: user.is_current,
|
||||
is_service_user: user.is_service_user,
|
||||
is_blocked: user.is_blocked
|
||||
} as User));
|
||||
dispatch(userActions.setEditUserPopupVisible(true));
|
||||
}
|
||||
|
||||
const handleBlockUser = (block: boolean, user: UserDataTable) => {
|
||||
if (block) {
|
||||
confirmModal.confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
title: "Are you sure you want to block " + user.name + "?",
|
||||
width: 600,
|
||||
content: <Space direction="vertical" size="small">
|
||||
<Paragraph>Blocking this user will disconnect their devices and disable dashboard access.</Paragraph>
|
||||
</Space>,
|
||||
onOk() {
|
||||
let userToSave = createUserToSave(user, block)
|
||||
dispatch(userActions.saveUser.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: userToSave
|
||||
}));
|
||||
},
|
||||
onCancel() {
|
||||
// noop
|
||||
},
|
||||
});
|
||||
} else {
|
||||
let userToSave = createUserToSave(user, block)
|
||||
dispatch(userActions.saveUser.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: userToSave
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
const createUserToSave = (values: UserDataTable, block: boolean): UserToSave => {
|
||||
return {
|
||||
id: values.id,
|
||||
role: values.role,
|
||||
name: values.name,
|
||||
groupsToCreate: Array.of(),
|
||||
auto_groups: values.auto_groups,
|
||||
is_service_user: values.is_service_user,
|
||||
is_blocked: block
|
||||
} as UserToSave
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container style={{padding: "0px"}}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Paragraph>Manage users and their
|
||||
permissions.{(window.location.hostname == "app.netbird.io") ? "Same-domain email users are added automatically on first sign-in." : ""}</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}>
|
||||
{(isNetBirdHosted() || isLocalDev()) &&
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
<Button type="primary" onClick={onClickInviteUser}>Invite user</Button>
|
||||
</Col>
|
||||
</Row>}
|
||||
</Col>
|
||||
</Row>
|
||||
{failed &&
|
||||
<Alert message={failed.message} description={failed.data ? failed.data.message : " "}
|
||||
type="error" showIcon
|
||||
closable/>
|
||||
}
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Table
|
||||
pagination={{
|
||||
pageSize,
|
||||
showSizeChanger: false,
|
||||
showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} users`)
|
||||
}}
|
||||
className="card-table"
|
||||
showSorterTooltip={false}
|
||||
scroll={{x: true}}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={dataTable}>
|
||||
<Column title="Email" dataIndex="email"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).email.includes(value)}
|
||||
sorter={(a, b) => ((a as any).email.localeCompare((b as any).email))}
|
||||
defaultSortOrder='ascend'
|
||||
render={(text, record, index) => {
|
||||
const btn = <Button type="text"
|
||||
onClick={() => handleEditUser(record as UserDataTable)}
|
||||
className="tooltip-label">
|
||||
<Text
|
||||
strong>{(text && text.trim() !== "") ? text : (record as User).id}</Text>
|
||||
|
||||
</Button>
|
||||
|
||||
if ((record as User).is_current) {
|
||||
return <div>{btn}
|
||||
<Tag color="blue">me</Tag>
|
||||
</div>
|
||||
}
|
||||
|
||||
if ((record as User).status === "invited") {
|
||||
return <div>{btn}
|
||||
<Tag color="gold">invited</Tag>
|
||||
</div>
|
||||
}
|
||||
|
||||
if ((record as User).status === "blocked") {
|
||||
return <div>{btn}
|
||||
<Tag color="red">blocked</Tag>
|
||||
</div>
|
||||
}
|
||||
|
||||
return btn
|
||||
}}
|
||||
/>
|
||||
<Column title="Name" dataIndex="name"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).name.includes(value)}
|
||||
sorter={(a, b) => ((a as any).name.localeCompare((b as any).name))}/>
|
||||
<Column title="Groups" dataIndex="groupsCount" align="center"
|
||||
render={(text, record: UserDataTable, index) => {
|
||||
return renderPopoverGroups(text, record.auto_groups, record)
|
||||
}}
|
||||
/>
|
||||
<Column title="Role" dataIndex="role"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).role.includes(value)}
|
||||
sorter={(a, b) => ((a as any).role.localeCompare((b as any).role))}/>
|
||||
{isAdmin && (
|
||||
<Column title="Block user" align="center" width="150px" dataIndex="is_blocked"
|
||||
render={(e, record: UserDataTable, index) => {
|
||||
let witch = <Switch size={"small"} checked={e}
|
||||
disabled={record.is_current}
|
||||
onClick={(active: boolean) => {
|
||||
handleBlockUser(active, record)
|
||||
}}
|
||||
/>
|
||||
|
||||
if (record.is_current) {
|
||||
return <Tooltip
|
||||
title="You can't block or unblock yourself">
|
||||
<Empty image={""} description={""} style={{height: "1px", width: "auto"}}/>
|
||||
</Tooltip>
|
||||
}
|
||||
|
||||
return witch
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Table>
|
||||
</Card>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<InviteUserPopup/>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RegularUsers;
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Input,
|
||||
Menu,
|
||||
message,
|
||||
Modal,
|
||||
Modal, Popover,
|
||||
Radio,
|
||||
RadioChangeEvent,
|
||||
Row,
|
||||
@@ -24,11 +24,11 @@ import {
|
||||
import {Container} from "../components/Container";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {Route} from "../store/route/types";
|
||||
import {Route, RouteToSave} from "../store/route/types";
|
||||
import {actions as routeActions} from "../store/route";
|
||||
import {actions as peerActions} from "../store/peer";
|
||||
import {filter, sortBy} from "lodash";
|
||||
import {ExclamationCircleOutlined, QuestionCircleOutlined} from "@ant-design/icons";
|
||||
import {EllipsisOutlined, ExclamationCircleOutlined, QuestionCircleOutlined} from "@ant-design/icons";
|
||||
import RouteUpdate from "../components/RouteUpdate";
|
||||
import tableSpin from "../components/Spin";
|
||||
import {
|
||||
@@ -41,16 +41,26 @@ import {
|
||||
transformDataTable,
|
||||
transformGroupedDataTable
|
||||
} from '../utils/routes'
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {Group} from "../store/group/types";
|
||||
import {TooltipPlacement} from "antd/es/tooltip";
|
||||
import {actions as groupActions} from "../store/group";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
import {usePageSizeHelpers} from "../utils/pageSize";
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
const {Column} = Table;
|
||||
const {confirm} = Modal;
|
||||
|
||||
export const Routes = () => {
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const {
|
||||
getGroupNamesFromIDs,
|
||||
} = useGetGroupTagHelpers()
|
||||
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const routes = useSelector((state: RootState) => state.route.data);
|
||||
const failed = useSelector((state: RootState) => state.route.failed);
|
||||
const loading = useSelector((state: RootState) => state.route.loading);
|
||||
@@ -58,24 +68,18 @@ export const Routes = () => {
|
||||
const savedRoute = useSelector((state: RootState) => state.route.savedRoute);
|
||||
const peers = useSelector((state: RootState) => state.peer.data)
|
||||
const loadingPeer = useSelector((state: RootState) => state.peer.loading);
|
||||
const setupNewRouteVisible = useSelector((state: RootState) => state.route.setupNewRouteVisible)
|
||||
const [showTutorial, setShowTutorial] = useState(true)
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [optionAllEnable, setOptionAllEnable] = useState('enabled');
|
||||
const [pageSize, setPageSize] = useState(5);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [dataTable, setDataTable] = useState([] as RouteDataTable[]);
|
||||
const [routeToAction, setRouteToAction] = useState(null as RouteDataTable | null);
|
||||
const [groupedDataTable, setGroupedDataTable] = useState([] as GroupedDataTable[]);
|
||||
const [expandRowsOnClick, setExpandRowsOnClick] = useState(true)
|
||||
const [groupPopupVisible, setGroupPopupVisible] = useState("")
|
||||
|
||||
const [peerNameToIP, peerIPToName] = initPeerMaps(peers);
|
||||
|
||||
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 itemsMenuAction = [
|
||||
@@ -83,10 +87,6 @@ export const Routes = () => {
|
||||
key: "view",
|
||||
label: (<Button type="text" block onClick={() => onClickViewRoute()}>View</Button>)
|
||||
},
|
||||
// {
|
||||
// key: "delete",
|
||||
// label: (<Button type="text" block onClick={() => showConfirmDeactivate()}>Deactivate</Button>)
|
||||
// },
|
||||
{
|
||||
key: "delete",
|
||||
label: (<Button type="text" block onClick={() => showConfirmDelete()}>Delete</Button>)
|
||||
@@ -99,17 +99,20 @@ export const Routes = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(routeActions.getRoutes.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
dispatch(routeActions.getRoutes.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
}, [peers])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(peerActions.getPeers.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
dispatch(peerActions.getPeers.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
}, [])
|
||||
|
||||
const filterGroupedDataTable = (routes: GroupedDataTable[]): GroupedDataTable[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let f: GroupedDataTable[] = filter(routes, (f) =>
|
||||
(f.network_id.toLowerCase().includes(t) || f.network.toLowerCase().includes(t) || f.description.toLowerCase().includes(t) || t === "")
|
||||
(f.network_id.toLowerCase().includes(t) || f.network.toLowerCase().includes(t) ||
|
||||
f.description.toLowerCase().includes(t) || t === "" ||
|
||||
getGroupNamesFromIDs(f.routesGroups).find(u => u.toLowerCase().trim().includes(t)) )
|
||||
) as GroupedDataTable[]
|
||||
if (optionAllEnable !== "all") {
|
||||
f = filter(f, (f) => f.enabled)
|
||||
@@ -118,7 +121,7 @@ export const Routes = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peerIPToName)))
|
||||
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peers)))
|
||||
}, [dataTable])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -126,12 +129,12 @@ export const Routes = () => {
|
||||
setShowTutorial(false)
|
||||
} else {
|
||||
setShowTutorial(isShowTutorial(routes))
|
||||
setDataTable(sortBy(transformDataTable(routes, peerIPToName), "network_id"))
|
||||
setDataTable(sortBy(transformDataTable(routes, peers), "network_id"))
|
||||
}
|
||||
}, [routes])
|
||||
|
||||
useEffect(() => {
|
||||
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peerIPToName)))
|
||||
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peers)))
|
||||
}, [textToSearch, optionAllEnable])
|
||||
|
||||
const styleNotification = {marginTop: 85}
|
||||
@@ -151,10 +154,19 @@ export const Routes = () => {
|
||||
dispatch(routeActions.setSavedRoute({...savedRoute, success: false}))
|
||||
dispatch(routeActions.resetSavedRoute(null))
|
||||
} else if (savedRoute.error) {
|
||||
let errorMsg = "Failed to update network route"
|
||||
switch (savedRoute.error.statusCode) {
|
||||
case 403:
|
||||
errorMsg = "Failed to update network route. You might not have enough permissions."
|
||||
break
|
||||
default:
|
||||
errorMsg = savedRoute.error.data.message ? savedRoute.error.data.message : errorMsg
|
||||
break
|
||||
}
|
||||
message.error({
|
||||
content: savedRoute.error.data ? savedRoute.error.data : savedRoute.error.message,
|
||||
content: errorMsg,
|
||||
key: saveKey,
|
||||
duration: 2,
|
||||
duration: 5,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(routeActions.setSavedRoute({...savedRoute, error: null}))
|
||||
@@ -168,7 +180,7 @@ export const Routes = () => {
|
||||
if (deletedRoute.loading) {
|
||||
message.loading({content: 'Deleting...', key: deleteKey, style})
|
||||
} else if (deletedRoute.success) {
|
||||
message.success({content: 'Route has been successfully disabled.', key: deleteKey, duration: 2, style})
|
||||
message.success({content: 'Route has been successfully deleted.', key: deleteKey, duration: 2, style})
|
||||
dispatch(routeActions.resetDeletedRoute(null))
|
||||
} else if (deletedRoute.error) {
|
||||
message.error({
|
||||
@@ -186,33 +198,26 @@ export const Routes = () => {
|
||||
};
|
||||
|
||||
const searchDataTable = () => {
|
||||
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peerIPToName)))
|
||||
setGroupedDataTable(filterGroupedDataTable(transformGroupedDataTable(routes, peers)))
|
||||
}
|
||||
|
||||
const onChangeAllEnabled = ({target: {value}}: RadioChangeEvent) => {
|
||||
setOptionAllEnable(value)
|
||||
}
|
||||
|
||||
const onChangePageSize = (value: string) => {
|
||||
setPageSize(parseInt(value.toString()))
|
||||
}
|
||||
|
||||
const showConfirmDelete = () => {
|
||||
let name = routeToAction ? routeToAction.network_id : '';
|
||||
confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
title: "Delete network route \"" + name + "\"",
|
||||
width: 600,
|
||||
content: <Space direction="vertical" size="small">
|
||||
{routeToAction &&
|
||||
<>
|
||||
<Title level={5}>Delete netowork route "{routeToAction ? routeToAction.network_id : ''}"</Title>
|
||||
<Paragraph>Are you sure you want to delete this route from your account?</Paragraph>
|
||||
</>
|
||||
}
|
||||
<Paragraph>Are you sure you want to delete this route from your account?</Paragraph>
|
||||
</Space>,
|
||||
okType: 'danger',
|
||||
onOk() {
|
||||
dispatch(routeActions.deleteRoute.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: routeToAction?.id || ''
|
||||
}));
|
||||
},
|
||||
@@ -224,7 +229,6 @@ export const Routes = () => {
|
||||
|
||||
|
||||
const onClickAddNewRoute = () => {
|
||||
dispatch(routeActions.setSetupNewRouteHA(true));
|
||||
dispatch(routeActions.setSetupNewRouteVisible(true));
|
||||
dispatch(routeActions.setRoute({
|
||||
network: '',
|
||||
@@ -239,17 +243,18 @@ export const Routes = () => {
|
||||
|
||||
const onClickViewRoute = () => {
|
||||
dispatch(routeActions.setSetupNewRouteHA(false));
|
||||
dispatch(routeActions.setSetupNewRouteVisible(true));
|
||||
dispatch(routeActions.setRoute({
|
||||
id: routeToAction?.id || null,
|
||||
network: routeToAction?.network,
|
||||
network_id: routeToAction?.network_id,
|
||||
description: routeToAction?.description,
|
||||
peer: peerToPeerIP(routeToAction!.peer, peerNameToIP[routeToAction!.peer]),
|
||||
peer: peerToPeerIP(routeToAction!.peer_name, routeToAction!.peer_ip),
|
||||
metric: routeToAction?.metric,
|
||||
masquerade: routeToAction?.masquerade,
|
||||
enabled: routeToAction?.enabled
|
||||
enabled: routeToAction?.enabled,
|
||||
groups: routeToAction?.groups
|
||||
} as Route))
|
||||
dispatch(routeActions.setSetupNewRouteVisible(true));
|
||||
}
|
||||
|
||||
const setRouteAndView = (route: RouteDataTable) => {
|
||||
@@ -261,10 +266,11 @@ export const Routes = () => {
|
||||
network: route.network,
|
||||
network_id: route.network_id,
|
||||
description: route.description,
|
||||
peer: route.peer ? peerToPeerIP(route.peer, peerNameToIP[route.peer]) : '',
|
||||
peer: route.peer ? peerToPeerIP(route.peer_name, route.peer_ip) : '',
|
||||
metric: route.metric ? route.metric : 9999,
|
||||
masquerade: route.masquerade,
|
||||
enabled: route.enabled
|
||||
enabled: route.enabled,
|
||||
groups: route.groups
|
||||
} as Route))
|
||||
dispatch(routeActions.setSetupNewRouteVisible(true));
|
||||
}
|
||||
@@ -293,17 +299,81 @@ export const Routes = () => {
|
||||
});
|
||||
}
|
||||
|
||||
const onPopoverVisibleChange = (b:boolean, key: string) => {
|
||||
if (setupNewRouteVisible) {
|
||||
setGroupPopupVisible("")
|
||||
} else {
|
||||
if(b) {
|
||||
setGroupPopupVisible(key)
|
||||
} else {
|
||||
setGroupPopupVisible("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleSwitchMasquerade(routeGroup: GroupedDataTable, checked: boolean) {
|
||||
routeGroup.groupedRoutes.forEach((record) => {
|
||||
const route = {
|
||||
...record,
|
||||
peer: peerNameToIP[record.peer],
|
||||
peer: record.peer,
|
||||
masquerade: checked,
|
||||
} as Route
|
||||
dispatch(routeActions.saveRoute.request({getAccessTokenSilently: getAccessTokenSilently, payload: route}));
|
||||
groupsToCreate: []
|
||||
} as RouteToSave
|
||||
dispatch(routeActions.saveRoute.request({getAccessTokenSilently: getTokenSilently, payload: route}));
|
||||
})
|
||||
}
|
||||
|
||||
const renderPopoverGroups = (label: string, rowGroups: string[] | null, userToAction: RouteDataTable) => {
|
||||
|
||||
let groupsMap = new Map<string, Group>();
|
||||
groups.forEach(g => {
|
||||
groupsMap.set(g.id!, g)
|
||||
})
|
||||
|
||||
let displayGroups: Group[] = []
|
||||
if (rowGroups) {
|
||||
displayGroups = rowGroups.filter(g => groupsMap.get(g)).map(g => groupsMap.get(g)!)
|
||||
}
|
||||
|
||||
let btn = <Button type="link" onClick={() => setRouteAndView(userToAction)}>{displayGroups.length}</Button>
|
||||
|
||||
if (!displayGroups || displayGroups!.length < 1) {
|
||||
return btn
|
||||
}
|
||||
|
||||
const content = displayGroups?.map((g, i) => {
|
||||
const _g = g as Group
|
||||
const peersCount = ` - ${_g.peers_count || 0} ${(!_g.peers_count || parseInt(_g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<div key={i}>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{_g.name}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
const mainContent = (<Space direction="vertical">{content}</Space>)
|
||||
let popoverPlacement = "top"
|
||||
if (content && content.length > 5) {
|
||||
popoverPlacement = "rightTop"
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover placement={popoverPlacement as TooltipPlacement}
|
||||
key={userToAction.id}
|
||||
onOpenChange={(b: boolean) => onPopoverVisibleChange(b, userToAction.key)}
|
||||
open={groupPopupVisible === userToAction.key}
|
||||
content={mainContent}
|
||||
title={null}>
|
||||
{btn}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
const expandedRowRender = (record: GroupedDataTable) => {
|
||||
|
||||
return <Table
|
||||
@@ -315,7 +385,7 @@ export const Routes = () => {
|
||||
size="small"
|
||||
bordered={true}
|
||||
>
|
||||
<Column title="Routing Peer" dataIndex="peer" align="center"
|
||||
<Column title="Routing Peer" dataIndex="peer_name" align="center"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).peer.includes(value)}
|
||||
sorter={(a, b) => ((a as any).peer.localeCompare((b as any).peer))}
|
||||
render={(text, record) => {
|
||||
@@ -327,7 +397,12 @@ export const Routes = () => {
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).metric.includes(value)}
|
||||
sorter={(a, b) => ((a as any).metric - ((b as any).metric))}
|
||||
/>
|
||||
<Column title="Status" dataIndex="enabled" align="center"
|
||||
<Column title="Groups" dataIndex="groupsCount" align="center"
|
||||
render={(text, record: RouteDataTable) => {
|
||||
return renderPopoverGroups(text, record.groups, record)
|
||||
}}
|
||||
/>
|
||||
<Column title="Routing peer status" dataIndex="enabled" align="center"
|
||||
render={(text: Boolean) => {
|
||||
return text ? <Tag color="green">enabled</Tag> : <Tag color="red">disabled</Tag>
|
||||
}}
|
||||
@@ -335,10 +410,17 @@ export const Routes = () => {
|
||||
<Column title="" align="center"
|
||||
render={(text, record) => {
|
||||
if (deletedRoute.loading || savedRoute.loading) return <></>
|
||||
return <Dropdown.Button type="text" overlay={actionsMenu} trigger={["click"]}
|
||||
onVisibleChange={visible => {
|
||||
if (visible) setRouteToAction(record as RouteDataTable)
|
||||
}}></Dropdown.Button>
|
||||
return (
|
||||
<Dropdown trigger={["click"]} overlay={actionsMenu} onOpenChange={visible => {
|
||||
if (visible) setRouteToAction(record as RouteDataTable)
|
||||
}}>
|
||||
<Button type="text">
|
||||
<Space>
|
||||
<EllipsisOutlined />
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
@@ -438,7 +520,7 @@ export const Routes = () => {
|
||||
sorter={(a, b) => ((a as any).network.localeCompare((b as any).network))}
|
||||
// defaultSortOrder='ascend'
|
||||
/>
|
||||
<Column title="Status" dataIndex="enabled" align="center"
|
||||
<Column title="Route status" dataIndex="enabled" align="center"
|
||||
render={(text: Boolean) => {
|
||||
return text ? <Tag color="green">enabled</Tag> :
|
||||
<Tag color="red">disabled</Tag>
|
||||
@@ -463,8 +545,8 @@ export const Routes = () => {
|
||||
if (count > 1) {
|
||||
tag = <Tag color="green">on</Tag>
|
||||
}
|
||||
return <div>{tag}<Divider type="vertical"/><Button type="link"
|
||||
onClick={() => setRouteAndView(record)}>Configure</Button>
|
||||
return <div>{tag}<Divider type="vertical"/>
|
||||
<Button type="link" onClick={() => setRouteAndView(record)}>Configure</Button>
|
||||
</div>
|
||||
}}
|
||||
/>
|
||||
|
||||
263
src/views/ServiceUsers.tsx
Normal file
263
src/views/ServiceUsers.tsx
Normal file
@@ -0,0 +1,263 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as userActions} from '../store/user';
|
||||
import {Container} from "../components/Container";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Input,
|
||||
message, Modal,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {User} from "../store/user/types";
|
||||
import {filter} from "lodash";
|
||||
import tableSpin from "../components/Spin";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {actions as groupActions} from "../store/group";
|
||||
import {capitalize, isLocalDev, isNetBirdHosted} from "../utils/common";
|
||||
import {usePageSizeHelpers} from "../utils/pageSize";
|
||||
import AddServiceUserPopup from "../components/popups/AddServiceUserPopup";
|
||||
import {ExclamationCircleOutlined} from "@ant-design/icons";
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
const {Column} = Table;
|
||||
|
||||
interface UserDataTable extends User {
|
||||
key: string
|
||||
}
|
||||
|
||||
const styleNotification = {marginTop: 85}
|
||||
|
||||
export const ServiceUsers = () => {
|
||||
const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const user = useSelector((state: RootState) => state.user.user)
|
||||
const users = useSelector((state: RootState) => state.user.serviceUsers);
|
||||
const failed = useSelector((state: RootState) => state.user.failed);
|
||||
const loading = useSelector((state: RootState) => state.user.loading);
|
||||
const updateUserDrawerVisible = useSelector((state: RootState) => state.user.updateUserDrawerVisible)
|
||||
const savedUser = useSelector((state: RootState) => state.user.savedUser)
|
||||
const deletedUser = useSelector((state: RootState) => state.user.deletedUser)
|
||||
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [dataTable, setDataTable] = useState([] as UserDataTable[]);
|
||||
|
||||
const transformDataTable = (d: User[]): UserDataTable[] => {
|
||||
return d.map(p => ({key: p.id, ...p} as UserDataTable))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(userActions.getServiceUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
}, [savedUser, deletedUser])
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(users))
|
||||
}, [users, groups])
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}, [textToSearch])
|
||||
|
||||
const filterDataTable = (): User[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let f: User[] = filter(users, (f: User) =>
|
||||
((f.email || f.id).toLowerCase().includes(t) || f.name.toLowerCase().includes(t) || f.role.includes(t) || t === "")
|
||||
) as User[]
|
||||
return f
|
||||
}
|
||||
|
||||
const onChangeTextToSearch = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setTextToSearch(e.target.value)
|
||||
};
|
||||
|
||||
const searchDataTable = () => {
|
||||
const data = filterDataTable()
|
||||
setDataTable(transformDataTable(data))
|
||||
}
|
||||
|
||||
const onClickCreateServiceUser = () => {
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
dispatch(userActions.setAddServiceUserPopupVisible(true));
|
||||
}
|
||||
|
||||
const createKey = 'saving';
|
||||
useEffect(() => {
|
||||
if (savedUser.loading) {
|
||||
message.loading({content: 'Saving...', key: createKey, duration: 0, style: styleNotification});
|
||||
} else if (savedUser.success) {
|
||||
message.success({
|
||||
content: 'User has been successfully saved.',
|
||||
key: createKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(userActions.setUpdateUserDrawerVisible(false));
|
||||
dispatch(userActions.setSavedUser({...savedUser, success: false}));
|
||||
dispatch(userActions.resetSavedUser(null))
|
||||
} else if (savedUser.error) {
|
||||
let errorMsg = "Failed to update user"
|
||||
switch (savedUser.error.statusCode) {
|
||||
case 412:
|
||||
case 403:
|
||||
if (savedUser.error.data) {
|
||||
errorMsg = capitalize(savedUser.error.data.message)
|
||||
}
|
||||
break
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: createKey,
|
||||
duration: 5,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(userActions.setSavedUser({...savedUser, error: null}));
|
||||
dispatch(userActions.resetSavedUser(null))
|
||||
}
|
||||
}, [savedUser])
|
||||
|
||||
const handleEditUser = (user: UserDataTable) => {
|
||||
dispatch(userActions.setUser({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
auto_groups: user.auto_groups ? user.auto_groups : [],
|
||||
name: user.name,
|
||||
is_current: user.is_current,
|
||||
is_service_user: user.is_service_user,
|
||||
} as User));
|
||||
dispatch(userActions.setEditUserPopupVisible(true));
|
||||
}
|
||||
|
||||
const handleDeleteUser = (user: UserDataTable) => {
|
||||
confirmModal.confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
title: "Delete token \"" + user.name + "\"",
|
||||
width: 600,
|
||||
content: <Space direction="vertical" size="small">
|
||||
<Paragraph>Are you sure you want to delete this service user?</Paragraph>
|
||||
</Space>,
|
||||
onOk() {
|
||||
dispatch(userActions.deleteUser.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: user.id
|
||||
}));
|
||||
dispatch(userActions.getServiceUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
},
|
||||
onCancel() {
|
||||
// noop
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!user && <Container style={{padding: "0px"}}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Paragraph style={{maxWidth: "70%"}}>Service users are non-login users that are not associated with any specific person. Network administrators
|
||||
use them to create tokens for API access to avoid losing automated access to critical systems when employees leave the company.</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 type="primary" onClick={onClickCreateServiceUser}>Create Service User</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} service users`)
|
||||
}}
|
||||
className="card-table"
|
||||
showSorterTooltip={false}
|
||||
scroll={{x: true}}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={dataTable}>
|
||||
<Column title="Name" dataIndex="name"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).name.includes(value)}
|
||||
sorter={(a, b) => ((a as any).name.localeCompare((b as any).name))}
|
||||
defaultSortOrder='ascend'
|
||||
render={(text, record, index) => {
|
||||
return <Button type="text"
|
||||
onClick={() => handleEditUser(record as UserDataTable)}>
|
||||
<Text strong>{(text && text.trim() !== "") ? text : (record as User).name}</Text>
|
||||
</Button>
|
||||
}}/>
|
||||
<Column title="Status" dataIndex="status"
|
||||
align="center"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).status.includes(value)}
|
||||
sorter={(a, b) => ((a as any).status.localeCompare((b as any).status))}
|
||||
render={(text, record, index) => {
|
||||
if (text == "active") {
|
||||
return <Tag color="green">{text}</Tag>
|
||||
} else if (text === "invited"){
|
||||
return <Tag color="gold">{text}</Tag>
|
||||
}
|
||||
return <Tag color="red">{text}</Tag>
|
||||
}}
|
||||
/>
|
||||
<Column title="Role" dataIndex="role"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).role.includes(value)}
|
||||
sorter={(a, b) => ((a as any).role.localeCompare((b as any).role))}/>
|
||||
<Column title="" align="center" width="250px"
|
||||
render={(text, record, index) => {
|
||||
return (
|
||||
<Button danger={true} type={"text"} style={{marginLeft: "3px", marginRight: "3px"}}
|
||||
onClick={() => {
|
||||
let userRecord = (record as UserDataTable)
|
||||
handleDeleteUser(userRecord)
|
||||
}}
|
||||
>Delete</Button>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
</Card>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>}
|
||||
<AddServiceUserPopup/>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServiceUsers;
|
||||
227
src/views/Settings.tsx
Normal file
227
src/views/Settings.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {Button, Card, Col, Form, List, message, Modal, Radio, Row, Space, Typography,} from "antd";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
import {Container} from "../components/Container";
|
||||
import ExpiresInInput, {expiresInToSeconds, secondsToExpiresIn} from "./ExpiresInInput";
|
||||
import {checkExpiresIn} from "../utils/common";
|
||||
import {actions as accountActions} from "../store/account";
|
||||
import {Account, FormAccount} from "../store/account/types";
|
||||
import {ExclamationCircleOutlined, QuestionCircleFilled} from "@ant-design/icons";
|
||||
|
||||
const {Title, Paragraph} = Typography;
|
||||
|
||||
const styleNotification = {marginTop: 85}
|
||||
|
||||
export const Settings = () => {
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const {
|
||||
} = useGetGroupTagHelpers()
|
||||
|
||||
const accounts = useSelector((state: RootState) => state.account.data);
|
||||
const failed = useSelector((state: RootState) => state.account.failed);
|
||||
const loading = useSelector((state: RootState) => state.account.loading);
|
||||
const updatedAccount = useSelector((state: RootState) => state.account.updatedAccount);
|
||||
const users = useSelector((state: RootState) => state.user.data);
|
||||
const [formAccount, setFormAccount] = useState({} as FormAccount);
|
||||
const [accountToAction, setAccountToAction] = useState({} as FormAccount);
|
||||
const [formPeerExpirationEnabled, setFormPeerExpirationEnabled] = useState(true);
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
|
||||
const [form] = Form.useForm()
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(accountActions.getAccounts.request({getAccessTokenSilently: getTokenSilently, payload: null}));
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (accounts.length < 1) {
|
||||
console.debug("invalid account data returned from the Management API", accounts)
|
||||
return
|
||||
}
|
||||
let account = accounts[0]
|
||||
|
||||
let fAccount = {
|
||||
id: account.id,
|
||||
settings: account.settings,
|
||||
peer_login_expiration_formatted: secondsToExpiresIn(account.settings.peer_login_expiration, ["hour", "day"]),
|
||||
peer_login_expiration_enabled: account.settings.peer_login_expiration_enabled
|
||||
} as FormAccount
|
||||
setFormAccount(fAccount)
|
||||
setFormPeerExpirationEnabled(fAccount.peer_login_expiration_enabled)
|
||||
form.setFieldsValue(fAccount)
|
||||
}, [accounts])
|
||||
|
||||
const updatingSettings = 'updating_settings';
|
||||
useEffect(() => {
|
||||
if (updatedAccount.loading) {
|
||||
message.loading({content: 'Saving...', key: updatingSettings, duration: 0, style: styleNotification});
|
||||
} else if (updatedAccount.success) {
|
||||
message.success({
|
||||
content: 'Account settings have been successfully saved.',
|
||||
key: updatingSettings,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(accountActions.setUpdateAccount({...updatedAccount, success: false}));
|
||||
dispatch(accountActions.resetUpdateAccount(null))
|
||||
let fAccount = {
|
||||
id: updatedAccount.data.id,
|
||||
settings: updatedAccount.data.settings,
|
||||
peer_login_expiration_formatted: secondsToExpiresIn(updatedAccount.data.settings.peer_login_expiration, ["hour", "day"]),
|
||||
peer_login_expiration_enabled: updatedAccount.data.settings.peer_login_expiration_enabled
|
||||
} as FormAccount
|
||||
setFormAccount(fAccount)
|
||||
} else if (updatedAccount.error) {
|
||||
let errorMsg = "Failed to update account settings"
|
||||
switch (updatedAccount.error.statusCode) {
|
||||
case 403:
|
||||
errorMsg = "Failed to update account settings. You might not have enough permissions."
|
||||
break
|
||||
default:
|
||||
errorMsg = updatedAccount.error.data.message ? updatedAccount.error.data.message : errorMsg
|
||||
break
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: updatingSettings,
|
||||
duration: 5,
|
||||
style: styleNotification
|
||||
});
|
||||
}
|
||||
}, [updatedAccount])
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
confirmSave(values)
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
let msg = "please check the fields and try again"
|
||||
if (errorInfo.errorFields) {
|
||||
msg = errorInfo.errorFields[0].errors[0]
|
||||
}
|
||||
message.error({
|
||||
content: msg,
|
||||
duration: 1,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const createAccountToSave = (values: FormAccount): Account => {
|
||||
return {
|
||||
id: formAccount.id,
|
||||
settings: {
|
||||
peer_login_expiration: expiresInToSeconds(values.peer_login_expiration_formatted),
|
||||
peer_login_expiration_enabled: values.peer_login_expiration_enabled
|
||||
}
|
||||
} as Account
|
||||
}
|
||||
|
||||
const confirmSave = (newValues: FormAccount) => {
|
||||
if (newValues.peer_login_expiration_enabled != formAccount.peer_login_expiration_enabled) {
|
||||
let content = newValues.peer_login_expiration_enabled ? "Enabling peer expiration will cause some peers added with the SSO login to disconnect, and re-authentication will be required. Do you want to enable peer login expiration?" : "Disabling peer expiration will cause peers added with the SSO login never to expire. For security reasons, keeping peers expiring periodically is usually better. Do you want to disable peer login expiration?"
|
||||
confirmModal.confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
title: "Before you update your account settings.",
|
||||
width: 600,
|
||||
content: content,
|
||||
onOk() {
|
||||
saveAccount(newValues)
|
||||
},
|
||||
onCancel() {
|
||||
},
|
||||
});
|
||||
} else {
|
||||
saveAccount(newValues)
|
||||
}
|
||||
}
|
||||
|
||||
const saveAccount = (newValues: FormAccount) => {
|
||||
let accountToSave = createAccountToSave(newValues)
|
||||
dispatch(accountActions.updateAccount.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: accountToSave
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container style={{paddingTop: "40px"}}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Settings</Title>
|
||||
<Paragraph>Manage your account's settings</Paragraph>
|
||||
<Space direction="vertical" size="large" style={{display: 'flex'}}>
|
||||
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Form
|
||||
name="basic"
|
||||
autoComplete="off"
|
||||
form={form}
|
||||
onFinish={handleFormSubmit}
|
||||
>
|
||||
<Space direction={"vertical"}
|
||||
style={{display: 'flex'}}>
|
||||
<Card
|
||||
title="Authentication"
|
||||
loading={loading}
|
||||
defaultValue={"Enabled"}
|
||||
>
|
||||
<Form.Item
|
||||
label="Peer login expiration"
|
||||
name="peer_login_expiration_enabled"
|
||||
tooltip="Peer login expiration allows to periodically request re-authentication of peers that were added with the SSO login. You can disable the expiration per peer in the peers tab."
|
||||
//rules={[{validator: selectValidatorEmptyStrings}]}
|
||||
>
|
||||
<Radio.Group
|
||||
options={[{label: 'Enabled', value: true}, {
|
||||
label: 'Disabled',
|
||||
value: false
|
||||
}]}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
onChange={function (e) {
|
||||
setFormPeerExpirationEnabled(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="peer_login_expiration_formatted"
|
||||
label="Peer login expires in"
|
||||
tooltip="Time after which every peer added with SSO login will require re-authentication."
|
||||
rules={[{validator: checkExpiresIn}]}>
|
||||
<ExpiresInInput
|
||||
disabled={!formPeerExpirationEnabled}
|
||||
options={Array.of({key: "hour", title: "Hours"}, {
|
||||
key: "day",
|
||||
title: "Days"
|
||||
})
|
||||
}/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
|
||||
href="https://docs.netbird.io/how-to/enforce-periodic-user-authentication">Learn more about login expiration</Button>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
<Form.Item style={{textAlign: 'center'}}>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Save
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Form>
|
||||
</Card>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
@@ -1,16 +1,14 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as setupKeyActions} from '../store/setup-key';
|
||||
import {Container} from "../components/Container";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "typesafe-actions";
|
||||
import { actions as setupKeyActions } from "../store/setup-key";
|
||||
import { Container } from "../components/Container";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Dropdown,
|
||||
Input,
|
||||
Menu,
|
||||
message,
|
||||
Modal,
|
||||
Popover,
|
||||
@@ -21,338 +19,334 @@ import {
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {SetupKey, SetupKeyToSave} from "../store/setup-key/types";
|
||||
import {filter} from "lodash"
|
||||
import {formatDate, timeAgo} from "../utils/common";
|
||||
import {ExclamationCircleOutlined} from "@ant-design/icons";
|
||||
import { SetupKey, SetupKeyToSave } from "../store/setup-key/types";
|
||||
import { filter } from "lodash";
|
||||
import { formatDate, timeAgo } from "../utils/common";
|
||||
import { ExclamationCircleOutlined } from "@ant-design/icons";
|
||||
import SetupKeyNew from "../components/SetupKeyNew";
|
||||
import ButtonCopyMessage from "../components/ButtonCopyMessage";
|
||||
import tableSpin from "../components/Spin";
|
||||
import {actions as groupActions} from "../store/group";
|
||||
import {Group} from "../store/group/types";
|
||||
import {TooltipPlacement} from "antd/es/tooltip";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import { actions as groupActions } from "../store/group";
|
||||
import { Group } from "../store/group/types";
|
||||
import { TooltipPlacement } from "antd/es/tooltip";
|
||||
import { useGetTokenSilently } from "../utils/token";
|
||||
import { usePageSizeHelpers } from "../utils/pageSize";
|
||||
|
||||
const {Title, Text, Paragraph} = Typography;
|
||||
const {Column} = Table;
|
||||
const {confirm} = Modal;
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
const { Column } = Table;
|
||||
|
||||
interface SetupKeyDataTable extends SetupKey {
|
||||
key: string
|
||||
groupsCount: number
|
||||
key: string;
|
||||
groupsCount: number;
|
||||
}
|
||||
|
||||
export const SetupKeys = () => {
|
||||
//const {accessToken} = useOidcAccessToken()
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const { onChangePageSize, pageSizeOptions, pageSize } = usePageSizeHelpers();
|
||||
const { getTokenSilently } = useGetTokenSilently();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const setupKeys = useSelector((state: RootState) => state.setupKey.data);
|
||||
const failed = useSelector((state: RootState) => state.setupKey.failed);
|
||||
const loading = useSelector((state: RootState) => state.setupKey.loading);
|
||||
const deletedSetupKey = useSelector((state: RootState) => state.setupKey.deletedSetupKey);
|
||||
const savedSetupKey = useSelector((state: RootState) => state.setupKey.savedSetupKey);
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const groups = useSelector((state: RootState) => state.group.data);
|
||||
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [optionValidAll, setOptionValidAll] = useState('valid');
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [textToSearch, setTextToSearch] = useState("");
|
||||
const [optionValidAll, setOptionValidAll] = useState("valid");
|
||||
const [dataTable, setDataTable] = useState([] as SetupKeyDataTable[]);
|
||||
const [setupKeyToAction, setSetupKeyToAction] = useState(null as SetupKeyDataTable | null);
|
||||
const setupNewKeyVisible = useSelector((state: RootState) => state.setupKey.setupNewKeyVisible)
|
||||
const [groupPopupVisible,setGroupPopupVisible] = useState(false as boolean|undefined)
|
||||
const setupNewKeyVisible = useSelector((state: RootState) => state.setupKey.setupNewKeyVisible);
|
||||
const [groupPopupVisible, setGroupPopupVisible] = useState("");
|
||||
|
||||
const styleNotification = {marginTop: 85}
|
||||
const styleNotification = { marginTop: 85 };
|
||||
const showTutorial = !dataTable.length;
|
||||
|
||||
const pageSizeOptions = [
|
||||
{label: "5", value: "5"},
|
||||
{label: "10", value: "10"},
|
||||
{label: "15", value: "15"}
|
||||
]
|
||||
const optionsValidAll = [
|
||||
{ label: "Valid", value: "valid" },
|
||||
{ label: "All", value: "all" },
|
||||
];
|
||||
|
||||
const optionsValidAll = [{label: 'Valid', value: 'valid'}, {label: 'All', value: 'all'}]
|
||||
|
||||
const itemsMenuAction = [
|
||||
{
|
||||
key: "revoke",
|
||||
label: (<Button type="text" onClick={() => showConfirmRevoke()}>Revoke</Button>)
|
||||
},
|
||||
{
|
||||
key: "edit",
|
||||
label: (<Button type="text" onClick={() => onClickEditSetupKey()}>View</Button>)
|
||||
},
|
||||
|
||||
]
|
||||
const actionsMenu = (<Menu items={itemsMenuAction}></Menu>)
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
|
||||
const transformDataTable = (d: SetupKey[]): SetupKeyDataTable[] => {
|
||||
return d.map(p => ({...p, groupsCount: p.auto_groups ? p.auto_groups.length : 0} as SetupKeyDataTable))
|
||||
}
|
||||
return d.map((p) => ({ ...p, groupsCount: p.auto_groups ? p.auto_groups.length : 0 } as SetupKeyDataTable));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setupKeyActions.getSetupKeys.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
}, [])
|
||||
dispatch(setupKeyActions.getSetupKeys.request({ getAccessTokenSilently: getTokenSilently, payload: null }));
|
||||
dispatch(groupActions.getGroups.request({ getAccessTokenSilently: getTokenSilently, payload: null }));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}, [setupKeys])
|
||||
setDataTable(transformDataTable(filterDataTable()));
|
||||
}, [setupKeys]);
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}, [textToSearch, optionValidAll])
|
||||
setDataTable(transformDataTable(filterDataTable()));
|
||||
}, [textToSearch, optionValidAll]);
|
||||
|
||||
const deleteKey = 'deleting';
|
||||
const deleteKey = "deleting";
|
||||
useEffect(() => {
|
||||
if (deletedSetupKey.loading) {
|
||||
message.loading({content: 'Deleting...', key: deleteKey, style: styleNotification});
|
||||
message.loading({ content: "Deleting...", key: deleteKey, style: styleNotification });
|
||||
} else if (deletedSetupKey.success) {
|
||||
message.success({
|
||||
content: 'Setup key has been successfully removed.',
|
||||
content: "Setup key has been successfully removed.",
|
||||
key: deleteKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
style: styleNotification,
|
||||
});
|
||||
dispatch(setupKeyActions.setDeleteSetupKey({...deletedSetupKey, success: false}))
|
||||
dispatch(setupKeyActions.resetDeletedSetupKey(null))
|
||||
dispatch(setupKeyActions.setDeleteSetupKey({ ...deletedSetupKey, success: false }));
|
||||
dispatch(setupKeyActions.resetDeletedSetupKey(null));
|
||||
} else if (deletedSetupKey.error) {
|
||||
message.error({
|
||||
content: 'Failed to delete setup key. You might not have enough permissions.',
|
||||
content: "Failed to delete setup key. You might not have enough permissions.",
|
||||
key: deleteKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
style: styleNotification,
|
||||
});
|
||||
dispatch(setupKeyActions.setDeleteSetupKey({...deletedSetupKey, error: null}))
|
||||
dispatch(setupKeyActions.resetDeletedSetupKey(null))
|
||||
dispatch(setupKeyActions.setDeleteSetupKey({ ...deletedSetupKey, error: null }));
|
||||
dispatch(setupKeyActions.resetDeletedSetupKey(null));
|
||||
}
|
||||
}, [deletedSetupKey])
|
||||
}, [deletedSetupKey]);
|
||||
|
||||
const createKey = 'saving';
|
||||
const createKey = "saving";
|
||||
useEffect(() => {
|
||||
if (savedSetupKey.loading) {
|
||||
message.loading({content: 'Saving...', key: createKey, duration: 0, style: styleNotification});
|
||||
message.loading({ content: "Saving...", key: createKey, duration: 0, style: styleNotification });
|
||||
} else if (savedSetupKey.success) {
|
||||
message.success({
|
||||
content: 'Setup key has been successfully saved.',
|
||||
content: "Setup key has been successfully saved.",
|
||||
key: createKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
style: styleNotification,
|
||||
});
|
||||
dispatch(setupKeyActions.setSetupNewKeyVisible(false));
|
||||
dispatch(setupKeyActions.setSavedSetupKey({...savedSetupKey, success: false}));
|
||||
dispatch(setupKeyActions.resetSavedSetupKey(null))
|
||||
dispatch(setupKeyActions.setSavedSetupKey({ ...savedSetupKey, success: false }));
|
||||
dispatch(setupKeyActions.resetSavedSetupKey(null));
|
||||
} else if (savedSetupKey.error) {
|
||||
message.error({
|
||||
content: 'Failed to update setup key. You might not have enough permissions.',
|
||||
content: "Failed to update setup key. You might not have enough permissions.",
|
||||
key: createKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
style: styleNotification,
|
||||
});
|
||||
dispatch(setupKeyActions.setSavedSetupKey({...savedSetupKey, error: null}));
|
||||
dispatch(setupKeyActions.resetSavedSetupKey(null))
|
||||
dispatch(setupKeyActions.setSavedSetupKey({ ...savedSetupKey, error: null }));
|
||||
dispatch(setupKeyActions.resetSavedSetupKey(null));
|
||||
}
|
||||
}, [savedSetupKey])
|
||||
}, [savedSetupKey]);
|
||||
|
||||
const filterDataTable = (): SetupKey[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let f: SetupKey[] = [...setupKeys]
|
||||
const t = textToSearch.toLowerCase().trim();
|
||||
let f: SetupKey[] = [...setupKeys];
|
||||
if (optionValidAll === "valid") {
|
||||
f = filter(setupKeys, (_f: SetupKey) => _f.valid && !_f.revoked)
|
||||
f = filter(setupKeys, (_f: SetupKey) => _f.valid && !_f.revoked);
|
||||
}
|
||||
f = filter(f, (_f: SetupKey) =>
|
||||
(_f.name.toLowerCase().includes(t) || _f.state.includes(t) || _f.type.toLowerCase().includes(t) || _f.key.toLowerCase().includes(t) || t === "")
|
||||
) as SetupKey[]
|
||||
return f
|
||||
}
|
||||
f = filter(
|
||||
f,
|
||||
(_f: SetupKey) =>
|
||||
_f.name.toLowerCase().includes(t) ||
|
||||
_f.state.includes(t) ||
|
||||
_f.type.toLowerCase().includes(t) ||
|
||||
_f.key.toLowerCase().includes(t) ||
|
||||
t === ""
|
||||
) as SetupKey[];
|
||||
return f;
|
||||
};
|
||||
|
||||
const onChangeTextToSearch = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setTextToSearch(e.target.value)
|
||||
setTextToSearch(e.target.value);
|
||||
};
|
||||
|
||||
const searchDataTable = () => {
|
||||
const data = filterDataTable()
|
||||
setDataTable(transformDataTable(data))
|
||||
}
|
||||
const data = filterDataTable();
|
||||
setDataTable(transformDataTable(data));
|
||||
};
|
||||
|
||||
const onChangeValidAll = ({target: {value}}: RadioChangeEvent) => {
|
||||
setOptionValidAll(value)
|
||||
}
|
||||
const onChangeValidAll = ({ target: { value } }: RadioChangeEvent) => {
|
||||
setOptionValidAll(value);
|
||||
};
|
||||
|
||||
const onChangePageSize = (value: string) => {
|
||||
setPageSize(parseInt(value.toString()))
|
||||
}
|
||||
|
||||
const showConfirmRevoke = () => {
|
||||
confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
const showConfirmRevoke = (setupKeyToAction: SetupKeyDataTable) => {
|
||||
let name = setupKeyToAction ? setupKeyToAction.name : "";
|
||||
confirmModal.confirm({
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
title: 'Revoke setupKey "' + name + '"',
|
||||
width: 600,
|
||||
content: <Space direction="vertical" size="small">
|
||||
{setupKeyToAction &&
|
||||
<>
|
||||
<Title level={5}>Revoke setupKey "{setupKeyToAction ? setupKeyToAction.name : ''}"</Title>
|
||||
<Paragraph>Are you sure you want to revoke key?</Paragraph>
|
||||
</>
|
||||
}
|
||||
</Space>,
|
||||
okType: 'danger',
|
||||
content: (
|
||||
<Space direction="vertical" size="small">
|
||||
<Paragraph>Are you sure you want to revoke key?</Paragraph>
|
||||
</Space>
|
||||
),
|
||||
onOk() {
|
||||
dispatch(setupKeyActions.saveSetupKey.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: {
|
||||
id: setupKeyToAction ? setupKeyToAction.id : null,
|
||||
revoked: true,
|
||||
name: setupKeyToAction ? setupKeyToAction.name : null,
|
||||
auto_groups: setupKeyToAction && setupKeyToAction.auto_groups ? setupKeyToAction.auto_groups : [],
|
||||
} as SetupKeyToSave
|
||||
}));
|
||||
},
|
||||
onCancel() {
|
||||
setSetupKeyToAction(null);
|
||||
dispatch(
|
||||
setupKeyActions.saveSetupKey.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: {
|
||||
id: setupKeyToAction ? setupKeyToAction.id : null,
|
||||
revoked: true,
|
||||
name: setupKeyToAction ? setupKeyToAction.name : null,
|
||||
auto_groups:
|
||||
setupKeyToAction && setupKeyToAction.auto_groups ? setupKeyToAction.auto_groups : [],
|
||||
} as SetupKeyToSave,
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onClickAddNewSetupKey = () => {
|
||||
const autoGroups : string[] = []
|
||||
const autoGroups: string[] = [];
|
||||
dispatch(setupKeyActions.setSetupNewKeyVisible(true));
|
||||
dispatch(setupKeyActions.setSetupKey({
|
||||
name: "",
|
||||
type: "one-off",
|
||||
auto_groups: autoGroups
|
||||
} as SetupKey))
|
||||
}
|
||||
dispatch(
|
||||
setupKeyActions.setSetupKey({
|
||||
name: "",
|
||||
type: "one-off",
|
||||
auto_groups: autoGroups,
|
||||
} as SetupKey)
|
||||
);
|
||||
};
|
||||
|
||||
const setKeyAndView = (key: SetupKeyDataTable) => {
|
||||
dispatch(setupKeyActions.setSetupNewKeyVisible(true));
|
||||
dispatch(setupKeyActions.setSetupKey({
|
||||
id: key?.id || null,
|
||||
key: key?.key,
|
||||
name: key?.name,
|
||||
revoked: key?.revoked,
|
||||
expires: key?.expires,
|
||||
state: key?.state,
|
||||
type: key?.type,
|
||||
used_times: key?.used_times,
|
||||
valid: key?.valid,
|
||||
auto_groups: key?.auto_groups,
|
||||
last_used: key?.last_used,
|
||||
usage_limit: key?.usage_limit
|
||||
} as SetupKey))
|
||||
}
|
||||
|
||||
const onClickEditSetupKey = () => {
|
||||
dispatch(setupKeyActions.setSetupNewKeyVisible(true));
|
||||
dispatch(setupKeyActions.setSetupKey({
|
||||
id: setupKeyToAction?.id || null,
|
||||
key: setupKeyToAction?.key,
|
||||
name: setupKeyToAction?.name,
|
||||
revoked: setupKeyToAction?.revoked,
|
||||
expires: setupKeyToAction?.expires,
|
||||
state: setupKeyToAction?.state,
|
||||
type: setupKeyToAction?.type,
|
||||
used_times: setupKeyToAction?.used_times,
|
||||
valid: setupKeyToAction?.valid,
|
||||
auto_groups: setupKeyToAction?.auto_groups,
|
||||
last_used: setupKeyToAction?.last_used,
|
||||
usage_limit: setupKeyToAction?.usage_limit
|
||||
} as SetupKey))
|
||||
}
|
||||
dispatch(
|
||||
setupKeyActions.setSetupKey({
|
||||
id: key?.id || null,
|
||||
key: key?.key,
|
||||
name: key?.name,
|
||||
revoked: key?.revoked,
|
||||
expires: key?.expires,
|
||||
state: key?.state,
|
||||
type: key?.type,
|
||||
used_times: key?.used_times,
|
||||
valid: key?.valid,
|
||||
auto_groups: key?.auto_groups,
|
||||
last_used: key?.last_used,
|
||||
usage_limit: key?.usage_limit,
|
||||
} as SetupKey)
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (setupNewKeyVisible) {
|
||||
setGroupPopupVisible(false)
|
||||
setGroupPopupVisible("");
|
||||
}
|
||||
}, [setupNewKeyVisible])
|
||||
}, [setupNewKeyVisible]);
|
||||
|
||||
const onPopoverVisibleChange = (b:boolean) => {
|
||||
const onPopoverVisibleChange = (b: boolean, key: string) => {
|
||||
if (setupNewKeyVisible) {
|
||||
setGroupPopupVisible(false)
|
||||
setGroupPopupVisible("");
|
||||
} else {
|
||||
setGroupPopupVisible(undefined)
|
||||
if (b) {
|
||||
setGroupPopupVisible(key);
|
||||
} else {
|
||||
setGroupPopupVisible("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const renderPopoverGroups = (label: string, rowGroups: string[] | string[] | null, setupKeyToAction: SetupKeyDataTable) => {
|
||||
};
|
||||
|
||||
const renderPopoverGroups = (
|
||||
label: string,
|
||||
rowGroups: string[] | string[] | null,
|
||||
setupKeyToAction: SetupKeyDataTable
|
||||
) => {
|
||||
let groupsMap = new Map<string, Group>();
|
||||
groups.forEach(g => {
|
||||
groupsMap.set(g.id!, g)
|
||||
})
|
||||
groups.forEach((g) => {
|
||||
groupsMap.set(g.id!, g);
|
||||
});
|
||||
|
||||
let displayGroups :Group[] = []
|
||||
let displayGroups: Group[] = [];
|
||||
if (rowGroups) {
|
||||
displayGroups = rowGroups.filter(g => groupsMap.get(g)).map(g => groupsMap.get(g)!)
|
||||
displayGroups = rowGroups.filter((g) => groupsMap.get(g)).map((g) => groupsMap.get(g)!);
|
||||
}
|
||||
|
||||
let btn = <Button type="link" onClick={() => setUpdateGroupsVisible(setupKeyToAction, true)}>{displayGroups.length}</Button>
|
||||
let btn = (
|
||||
<Button type="link" onClick={() => setUpdateGroupsVisible(setupKeyToAction, true)}>
|
||||
{displayGroups.length}
|
||||
</Button>
|
||||
);
|
||||
if (!displayGroups || displayGroups!.length < 1) {
|
||||
return btn
|
||||
return btn;
|
||||
}
|
||||
|
||||
const content = displayGroups?.map((g, i) => {
|
||||
const _g = g as Group
|
||||
const peersCount = ` - ${_g.peers_count || 0} ${(!_g.peers_count || parseInt(_g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
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}}
|
||||
>
|
||||
<Tag color="blue" style={{ marginRight: 3 }}>
|
||||
<strong>{_g.name}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
<span style={{ fontSize: ".85em" }}>{peersCount}</span>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
const mainContent = (<Space direction="vertical">{content}</Space>)
|
||||
let popoverPlacement = "top"
|
||||
);
|
||||
});
|
||||
const mainContent = <Space direction="vertical">{content}</Space>;
|
||||
let popoverPlacement = "top";
|
||||
if (content && content.length > 5) {
|
||||
popoverPlacement = "rightTop"
|
||||
popoverPlacement = "rightTop";
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover placement={popoverPlacement as TooltipPlacement}
|
||||
key={setupKeyToAction.key}
|
||||
onOpenChange={onPopoverVisibleChange}
|
||||
open={groupPopupVisible}
|
||||
content={mainContent}
|
||||
title={null}>
|
||||
<Popover
|
||||
placement={popoverPlacement as TooltipPlacement}
|
||||
key={setupKeyToAction.key}
|
||||
onOpenChange={(b: boolean) => onPopoverVisibleChange(b, setupKeyToAction.key)}
|
||||
open={groupPopupVisible === setupKeyToAction.key}
|
||||
content={mainContent}
|
||||
title={null}
|
||||
>
|
||||
{btn}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const setUpdateGroupsVisible = (setupKeyToAction: SetupKey, status: boolean) => {
|
||||
if (status) {
|
||||
dispatch(setupKeyActions.setSetupKey({...setupKeyToAction}))
|
||||
dispatch(setupKeyActions.setSetupNewKeyVisible(true))
|
||||
return
|
||||
dispatch(setupKeyActions.setSetupKey({ ...setupKeyToAction }));
|
||||
dispatch(setupKeyActions.setSetupNewKeyVisible(true));
|
||||
return;
|
||||
}
|
||||
const autoGroups : string[] = []
|
||||
dispatch(setupKeyActions.setSetupKey({
|
||||
name: "",
|
||||
type: "one-off",
|
||||
auto_groups: autoGroups
|
||||
} as SetupKey))
|
||||
dispatch(setupKeyActions.setSetupNewKeyVisible(false))
|
||||
}
|
||||
const autoGroups: string[] = [];
|
||||
dispatch(
|
||||
setupKeyActions.setSetupKey({
|
||||
name: "",
|
||||
type: "one-off",
|
||||
auto_groups: autoGroups,
|
||||
} as SetupKey)
|
||||
);
|
||||
dispatch(setupKeyActions.setSetupNewKeyVisible(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container style={{paddingTop: "40px"}}>
|
||||
<Container style={{ paddingTop: "40px" }}>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Setup Keys</Title>
|
||||
<Paragraph>A list of all the setup keys in your account including their name, state, type and
|
||||
expiration.</Paragraph>
|
||||
<Space direction="vertical" size="large" style={{display: 'flex'}}>
|
||||
<Paragraph
|
||||
style={{
|
||||
color: dataTable.length ? "black" : "#818183",
|
||||
}}
|
||||
>
|
||||
A list of all the setup keys in your account including their name, state, type and
|
||||
expiration.
|
||||
</Paragraph>
|
||||
<Space direction="vertical" size="large" style={{ display: "flex" }}>
|
||||
<Row gutter={[16, 24]}>
|
||||
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
|
||||
{/*<Input.Search allowClear value={textToSearch} onPressEnter={searchDataTable} onSearch={searchDataTable} placeholder="Search..." onChange={onChangeTextToSearch} />*/}
|
||||
<Input allowClear value={textToSearch} onPressEnter={searchDataTable}
|
||||
placeholder="Search..." onChange={onChangeTextToSearch}/>
|
||||
<Input
|
||||
allowClear
|
||||
value={textToSearch}
|
||||
onPressEnter={searchDataTable}
|
||||
placeholder="Search..."
|
||||
onChange={onChangeTextToSearch}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
|
||||
<Space size="middle">
|
||||
@@ -362,117 +356,240 @@ export const SetupKeys = () => {
|
||||
value={optionValidAll}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
disabled={!dataTable?.length}
|
||||
/>
|
||||
<Select
|
||||
disabled={!dataTable?.length}
|
||||
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}
|
||||
sm={24}
|
||||
md={5}
|
||||
lg={5}
|
||||
xl={5}
|
||||
xxl={5} span={5}>
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
<Button type="primary" onClick={onClickAddNewSetupKey}>Add Key</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} setup keys`)
|
||||
}}
|
||||
className="card-table"
|
||||
showSorterTooltip={false}
|
||||
scroll={{x: true}}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={dataTable}>
|
||||
<Column title="Name" dataIndex="name"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).name.includes(value)}
|
||||
sorter={(a, b) => ((a as any).name.localeCompare((b as any).name))}
|
||||
render={(text, record, index) => {
|
||||
return <Button type="text"
|
||||
onClick={() => setKeyAndView(record as SetupKeyDataTable)}
|
||||
className="tooltip-label"> <Text strong>{text}</Text>
|
||||
{dataTable.length ? (
|
||||
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
<Button type="primary" onClick={onClickAddNewSetupKey}>
|
||||
Add key
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Row>
|
||||
{failed && (
|
||||
<Alert
|
||||
message={failed.message}
|
||||
description={failed.data ? failed.data.message : " "}
|
||||
type="error"
|
||||
showIcon
|
||||
closable
|
||||
/>
|
||||
)}
|
||||
{showTutorial ? (
|
||||
<Space
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: "white",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
border: "1px solid #f0f0f0",
|
||||
borderRadius: "4px",
|
||||
gap: "8px",
|
||||
}}
|
||||
>
|
||||
<Container
|
||||
style={{
|
||||
textAlign: "center",
|
||||
width: "615px",
|
||||
}}
|
||||
>
|
||||
<Col style={{ marginTop: "41px" }}>
|
||||
<Text
|
||||
style={{
|
||||
fontWeight: "600",
|
||||
fontSize: "22px",
|
||||
lineHeight: "26px",
|
||||
color: "#252526",
|
||||
}}
|
||||
>
|
||||
Create setup key
|
||||
</Text>
|
||||
</Col>
|
||||
<Col style={{ marginTop: "17px" }}>
|
||||
<Text
|
||||
style={{
|
||||
fontWeight: "400",
|
||||
fontSize: "14px",
|
||||
lineHeight: "22px",
|
||||
}}
|
||||
>
|
||||
Manage setup keys to register new machines in your network. The key
|
||||
links machines to your account during initial setup.
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://docs.netbird.io/how-to/register-machines-using-setup-keys"
|
||||
>
|
||||
{" "}
|
||||
Learn more
|
||||
</a>
|
||||
</Text>
|
||||
</Col>
|
||||
<Col
|
||||
style={{
|
||||
marginTop: "16px",
|
||||
marginBottom: "112px",
|
||||
}}
|
||||
defaultSortOrder='ascend'
|
||||
/>
|
||||
|
||||
<Column title="State" dataIndex="state"
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
padding: "6.4px, 15px",
|
||||
gap: "10px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
onClick={onClickAddNewSetupKey}
|
||||
>
|
||||
Add key
|
||||
</Button>
|
||||
</Col>
|
||||
</Container>
|
||||
</Space>
|
||||
) : (
|
||||
<Card bodyStyle={{ padding: 0 }}>
|
||||
<Table
|
||||
pagination={{
|
||||
pageSize,
|
||||
showSizeChanger: false,
|
||||
showTotal: (total, range) =>
|
||||
`Showing ${range[0]} to ${range[1]} of ${total} setup keys`,
|
||||
}}
|
||||
className="card-table"
|
||||
showSorterTooltip={false}
|
||||
scroll={{ x: true }}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={dataTable}
|
||||
>
|
||||
<Column
|
||||
title="Name"
|
||||
dataIndex="name"
|
||||
onFilter={(value: string | number | boolean, record) =>
|
||||
(record as any).name.includes(value)
|
||||
}
|
||||
sorter={(a, b) => (a as any).name.localeCompare((b as any).name)}
|
||||
render={(text, record, index) => {
|
||||
return (text === 'valid') ? <Tag color="green">{text}</Tag> :
|
||||
<Tag color="red">{text}</Tag>
|
||||
return (
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => setKeyAndView(record as SetupKeyDataTable)}
|
||||
className="tooltip-label"
|
||||
>
|
||||
{" "}
|
||||
<Text strong>{text}</Text>
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
sorter={(a, b) => ((a as any).state.localeCompare((b as any).state))}
|
||||
/>
|
||||
defaultSortOrder="ascend"
|
||||
/>
|
||||
<Column
|
||||
title="Type"
|
||||
dataIndex="type"
|
||||
onFilter={(value: string | number | boolean, record) =>
|
||||
(record as any).type.includes(value)
|
||||
}
|
||||
sorter={(a, b) => (a as any).type.localeCompare((b as any).type)}
|
||||
/>
|
||||
<Column
|
||||
title="Key"
|
||||
dataIndex="key"
|
||||
onFilter={(value: string | number | boolean, record) =>
|
||||
(record as any).key.includes(value)
|
||||
}
|
||||
sorter={(a, b) => (a as any).key.localeCompare((b as any).key)}
|
||||
render={(text, record, index) => {
|
||||
const body = <Text>{text}</Text>;
|
||||
return (
|
||||
<ButtonCopyMessage
|
||||
keyMessage={(record as SetupKeyDataTable).key}
|
||||
toCopy={text}
|
||||
body={body}
|
||||
messageText={"Key copied"}
|
||||
styleNotification={{}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Column title="Type" dataIndex="type"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).type.includes(value)}
|
||||
sorter={(a, b) => ((a as any).type.localeCompare((b as any).type))}
|
||||
/>
|
||||
<Column title="Groups" dataIndex="groupsCount" align="center"
|
||||
<Column
|
||||
title="Last Used"
|
||||
dataIndex="last_used"
|
||||
sorter={(a, b) => (a as any).last_used.localeCompare((b as any).last_used)}
|
||||
render={(text, record, index) => {
|
||||
return !(record as SetupKey).used_times ? "never" : timeAgo(text);
|
||||
}}
|
||||
/>
|
||||
<Column
|
||||
title="Groups"
|
||||
dataIndex="groupsCount"
|
||||
align="center"
|
||||
render={(text, record: SetupKeyDataTable, index) => {
|
||||
return renderPopoverGroups(text, record.auto_groups, record)
|
||||
return renderPopoverGroups(text, record.auto_groups, record);
|
||||
}}
|
||||
/>
|
||||
<Column title="Key" dataIndex="key"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).key.includes(value)}
|
||||
sorter={(a, b) => ((a as any).key.localeCompare((b as any).key))}
|
||||
/>
|
||||
<Column
|
||||
title="Expires"
|
||||
dataIndex="expires"
|
||||
render={(text, record, index) => {
|
||||
const body = <Text>{text}</Text>
|
||||
return <ButtonCopyMessage keyMessage={(record as SetupKeyDataTable).key}
|
||||
toCopy={text}
|
||||
body={body} messageText={"Key copied"}
|
||||
styleNotification={{}}/>
|
||||
return formatDate(text);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Column title="Last Used" dataIndex="last_used"
|
||||
sorter={(a, b) => ((a as any).last_used.localeCompare((b as any).last_used))}
|
||||
/>
|
||||
<Column
|
||||
title="State"
|
||||
dataIndex="state"
|
||||
render={(text, record, index) => {
|
||||
return !(record as SetupKey).used_times ? 'never' : timeAgo(text)
|
||||
return text === "valid" ? (
|
||||
<Tag color="green">{text}</Tag>
|
||||
) : (
|
||||
<Tag color="red">{text}</Tag>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Column title="Used Times" dataIndex="used_times"
|
||||
sorter={(a, b) => ((a as any).used_times - ((b as any).used_times))}
|
||||
/>
|
||||
|
||||
<Column title="Expires" dataIndex="expires"
|
||||
sorter={(a, b) => (a as any).state.localeCompare((b as any).state)}
|
||||
/>
|
||||
<Column
|
||||
title=""
|
||||
align="center"
|
||||
render={(text, record, index) => {
|
||||
return formatDate(text)
|
||||
return (
|
||||
<Button
|
||||
style={{
|
||||
color: "rgba(210, 64, 64, 0.85)",
|
||||
}}
|
||||
type="text"
|
||||
onClick={() => showConfirmRevoke(record as SetupKeyDataTable)}
|
||||
>
|
||||
Revoke
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Column title="" align="center"
|
||||
render={(text, record, index) => {
|
||||
return !(record as SetupKeyDataTable).revoked ? (
|
||||
<Dropdown.Button type="text" overlay={actionsMenu}
|
||||
trigger={["click"]}
|
||||
onOpenChange={visible => {
|
||||
if (visible) setSetupKeyToAction(record as SetupKeyDataTable)
|
||||
}}></Dropdown.Button>) : <></>
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
</Card>
|
||||
/>
|
||||
</Table>
|
||||
</Card>
|
||||
)}
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
</Container>
|
||||
<SetupKeyNew/>
|
||||
{setupNewKeyVisible && <SetupKeyNew />}
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default SetupKeys;
|
||||
@@ -1,373 +1,82 @@
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
|
||||
import {Col, Row, Tabs, TabsProps} from "antd";
|
||||
import {Container} from "../components/Container";
|
||||
import {actions as userActions} from "../store/user";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as userActions} from '../store/user';
|
||||
import {Container} from "../components/Container";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Dropdown,
|
||||
Input,
|
||||
Menu,
|
||||
message,
|
||||
Popover,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import {User} from "../store/user/types";
|
||||
import {filter} from "lodash";
|
||||
import tableSpin from "../components/Spin";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import UserUpdate from "../components/UserUpdate";
|
||||
import {actions as groupActions} from "../store/group";
|
||||
import {Group} from "../store/group/types";
|
||||
import {TooltipPlacement} from "antd/es/tooltip";
|
||||
import {useOidcUser} from "@axa-fr/react-oidc";
|
||||
import {Link} from "react-router-dom";
|
||||
import {actions as setupKeyActions} from "../store/setup-key";
|
||||
import {SetupKey} from "../store/setup-key/types";
|
||||
import {isLocalDev, isNetBirdHosted} from "../utils/common";
|
||||
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
const {Column} = Table;
|
||||
|
||||
interface UserDataTable extends User {
|
||||
key: string
|
||||
}
|
||||
|
||||
const styleNotification = {marginTop: 85}
|
||||
import RegularUsers from "./RegularUsers";
|
||||
import ServiceUsers from "./ServiceUsers";
|
||||
import UserEdit from "../components/UserEdit";
|
||||
|
||||
export const Users = () => {
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const {oidcUser} = useOidcUser();
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const users = useSelector((state: RootState) => state.user.data);
|
||||
const failed = useSelector((state: RootState) => state.user.failed);
|
||||
const loading = useSelector((state: RootState) => state.user.loading);
|
||||
const updateUserDrawerVisible = useSelector((state: RootState) => state.user.updateUserDrawerVisible)
|
||||
const savedUser = useSelector((state: RootState) => state.user.savedUser)
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const users = useSelector((state: RootState) => state.user.data)
|
||||
const user = useSelector((state: RootState) => state.user.user)
|
||||
const tab = useSelector((state: RootState) => state.user.userTabOpen)
|
||||
|
||||
const [groupPopupVisible, setGroupPopupVisible] = useState(false as boolean | undefined)
|
||||
const [userToAction, setUserToAction] = useState(null as UserDataTable | null);
|
||||
const [textToSearch, setTextToSearch] = useState('');
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [dataTable, setDataTable] = useState([] as UserDataTable[]);
|
||||
const [currentUser, setCurrentUser] = useState({} as User)
|
||||
const pageSizeOptions = [
|
||||
{label: "5", value: "5"},
|
||||
{label: "10", value: "10"},
|
||||
{label: "15", value: "15"}
|
||||
const userItems: TabsProps['items'] = [
|
||||
{
|
||||
key: 'Users',
|
||||
label: 'Users',
|
||||
children: <RegularUsers/>,
|
||||
},
|
||||
]
|
||||
|
||||
// setUserAndView makes the UserUpdate drawer visible (right side) and sets the user object
|
||||
const setUserAndView = (user: User) => {
|
||||
dispatch(userActions.setUpdateUserDrawerVisible(true));
|
||||
dispatch(userActions.setUser({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
auto_groups: user.auto_groups ? user.auto_groups : [],
|
||||
name: user.name
|
||||
} as User));
|
||||
}
|
||||
const adminOnlyItems: TabsProps['items'] = [
|
||||
{
|
||||
key: 'Service Users',
|
||||
label: 'Service Users',
|
||||
children: <ServiceUsers/>,
|
||||
},
|
||||
]
|
||||
|
||||
const transformDataTable = (d: User[]): UserDataTable[] => {
|
||||
return d.map(p => ({key: p.id, ...p} as UserDataTable))
|
||||
}
|
||||
const [tabItems, setTabItems] = useState(userItems);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(userActions.getUsers.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
|
||||
}, [])
|
||||
if (isAdmin) {
|
||||
setTabItems(userItems.concat(adminOnlyItems));
|
||||
}
|
||||
}, [isAdmin])
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(users))
|
||||
if(users) {
|
||||
let currentUser = users.find((user) => user.is_current)
|
||||
if(currentUser) {
|
||||
setIsAdmin(currentUser.role === 'admin');
|
||||
}
|
||||
}
|
||||
}, [users])
|
||||
|
||||
useEffect(() => {
|
||||
setDataTable(transformDataTable(filterDataTable()))
|
||||
}, [textToSearch])
|
||||
dispatch(userActions.getUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (oidcUser && oidcUser.sub) {
|
||||
const found = users.find(u => u.id == oidcUser.sub)
|
||||
if (found) {
|
||||
setCurrentUser(found)
|
||||
}
|
||||
} else {
|
||||
setCurrentUser({} as User)
|
||||
}
|
||||
|
||||
}, [oidcUser, users])
|
||||
|
||||
const filterDataTable = (): User[] => {
|
||||
const t = textToSearch.toLowerCase().trim()
|
||||
let f: User[] = filter(users, (f: User) =>
|
||||
((f.email || f.id).toLowerCase().includes(t) || f.name.includes(t) || f.role.includes(t) || t === "")
|
||||
) as User[]
|
||||
return f
|
||||
const onTabClick = (key:string) => {
|
||||
dispatch(userActions.setUserTabOpen(key))
|
||||
}
|
||||
|
||||
const onChangeTextToSearch = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setTextToSearch(e.target.value)
|
||||
};
|
||||
|
||||
const searchDataTable = () => {
|
||||
const data = filterDataTable()
|
||||
setDataTable(transformDataTable(data))
|
||||
}
|
||||
|
||||
const onChangePageSize = (value: string) => {
|
||||
setPageSize(parseInt(value.toString()))
|
||||
}
|
||||
|
||||
const onClickEdit = () => {
|
||||
dispatch(userActions.setUpdateUserDrawerVisible(true));
|
||||
dispatch(userActions.setUser({
|
||||
id: userToAction?.id,
|
||||
email: userToAction?.email,
|
||||
auto_groups: userToAction?.auto_groups ? userToAction?.auto_groups : [],
|
||||
name: userToAction?.name,
|
||||
role: userToAction?.role,
|
||||
} as User));
|
||||
}
|
||||
|
||||
const onClickInviteUser = () => {
|
||||
const autoGroups : string[] = []
|
||||
dispatch(userActions.setUpdateUserDrawerVisible(true));
|
||||
dispatch(userActions.setUser({
|
||||
id: "",
|
||||
email: "",
|
||||
auto_groups: autoGroups,
|
||||
name: "",
|
||||
role: "user",
|
||||
} as User));
|
||||
}
|
||||
|
||||
const renderPopoverGroups = (label: string, rowGroups: string[] | string[] | null, userToAction: UserDataTable) => {
|
||||
|
||||
let groupsMap = new Map<string, Group>();
|
||||
groups.forEach(g => {
|
||||
groupsMap.set(g.id!, g)
|
||||
})
|
||||
|
||||
let displayGroups: Group[] = []
|
||||
if (rowGroups) {
|
||||
displayGroups = rowGroups.filter(g => groupsMap.get(g)).map(g => groupsMap.get(g)!)
|
||||
}
|
||||
|
||||
let btn = <Button type="link" onClick={() => setUserAndView(userToAction)}>{displayGroups.length}</Button>
|
||||
if (!displayGroups || displayGroups!.length < 1) {
|
||||
return btn
|
||||
}
|
||||
|
||||
const content = displayGroups?.map((g, i) => {
|
||||
const _g = g as Group
|
||||
const peersCount = ` - ${_g.peers_count || 0} ${(!_g.peers_count || parseInt(_g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<div key={i}>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{_g.name}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
const mainContent = (<Space direction="vertical">{content}</Space>)
|
||||
let popoverPlacement = "top"
|
||||
if (content && content.length > 5) {
|
||||
popoverPlacement = "rightTop"
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover placement={popoverPlacement as TooltipPlacement}
|
||||
key={userToAction.id}
|
||||
onOpenChange={onPopoverVisibleChange}
|
||||
open={groupPopupVisible}
|
||||
content={mainContent}
|
||||
title={null}>
|
||||
{btn}
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (updateUserDrawerVisible) {
|
||||
setGroupPopupVisible(false)
|
||||
}
|
||||
}, [updateUserDrawerVisible])
|
||||
|
||||
const createKey = 'saving';
|
||||
useEffect(() => {
|
||||
if (savedUser.loading) {
|
||||
message.loading({content: 'Saving...', key: createKey, duration: 0, style: styleNotification});
|
||||
} else if (savedUser.success) {
|
||||
message.success({
|
||||
content: 'User has been successfully saved.',
|
||||
key: createKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(userActions.setUpdateUserDrawerVisible(false));
|
||||
dispatch(userActions.setSavedUser({...savedUser, success: false}));
|
||||
dispatch(userActions.resetSavedUser(null))
|
||||
} else if (savedUser.error) {
|
||||
let errorMsg = "Failed to update user"
|
||||
switch (savedUser.error.statusCode) {
|
||||
case 412:
|
||||
errorMsg = savedUser.error.data
|
||||
break
|
||||
case 403:
|
||||
errorMsg = "Failed to update user. You might not have enough permissions."
|
||||
break
|
||||
}
|
||||
message.error({
|
||||
content: errorMsg,
|
||||
key: createKey,
|
||||
duration: 5,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(userActions.setSavedUser({...savedUser, error: null}));
|
||||
dispatch(userActions.resetSavedUser(null))
|
||||
}
|
||||
}, [savedUser])
|
||||
|
||||
const onPopoverVisibleChange = (b: boolean) => {
|
||||
if (updateUserDrawerVisible) {
|
||||
setGroupPopupVisible(false)
|
||||
} else {
|
||||
setGroupPopupVisible(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
const itemsMenuAction = [
|
||||
{
|
||||
key: "edit",
|
||||
label: (<Button type="text" onClick={() => onClickEdit()}>View</Button>)
|
||||
},
|
||||
|
||||
]
|
||||
const actionsMenu = (<Menu items={itemsMenuAction}></Menu>)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container style={{paddingTop: "40px"}}>
|
||||
<Row>
|
||||
{!user && <Row>
|
||||
<Col span={24}>
|
||||
<Title level={4}>Users</Title>
|
||||
<Paragraph>A list of all users{(window.location.hostname == "app.netbird.io") ? ". Users with an email from the same private organization domain will automatically join when they sign in for the first time." : ""}</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}>
|
||||
{(isNetBirdHosted() || isLocalDev()) &&
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
<Button type="primary" onClick={onClickInviteUser}>Invite User</Button>
|
||||
</Col>
|
||||
</Row>}
|
||||
</Col>
|
||||
</Row>
|
||||
{failed &&
|
||||
<Alert message={failed.message} description={failed.data ? failed.data.message : " "} type="error" showIcon
|
||||
closable/>
|
||||
}
|
||||
<Card bodyStyle={{padding: 0}}>
|
||||
<Table
|
||||
pagination={{
|
||||
pageSize,
|
||||
showSizeChanger: false,
|
||||
showTotal: ((total, range) => `Showing ${range[0]} to ${range[1]} of ${total} users`)
|
||||
}}
|
||||
className="card-table"
|
||||
showSorterTooltip={false}
|
||||
scroll={{x: true}}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={dataTable}>
|
||||
<Column title="Email" dataIndex="email"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).email.includes(value)}
|
||||
sorter={(a, b) => ((a as any).email.localeCompare((b as any).email))}
|
||||
defaultSortOrder='ascend'
|
||||
render={(text, record, index) => {
|
||||
const btn = <Button type="text"
|
||||
onClick={() => setUserAndView(record as UserDataTable)}
|
||||
className="tooltip-label">
|
||||
<Text strong>{(text && text.trim() !== "") ? text : (record as User).id}</Text>
|
||||
</Button>
|
||||
if ((record as User).id !== currentUser.id) {
|
||||
return btn
|
||||
}
|
||||
|
||||
return <div>{btn}
|
||||
<Tag color="blue">me</Tag>
|
||||
</div>
|
||||
}}
|
||||
/>
|
||||
<Column title="Name" dataIndex="name"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).name.includes(value)}
|
||||
sorter={(a, b) => ((a as any).name.localeCompare((b as any).name))}/>
|
||||
<Column title="Status" dataIndex="status"
|
||||
align="center"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).status.includes(value)}
|
||||
sorter={(a, b) => ((a as any).status.localeCompare((b as any).status))}
|
||||
render={(text, record, index) => {
|
||||
if (text == "active") {
|
||||
return <Tag color="green">{text}</Tag>
|
||||
} else if (text === "invited"){
|
||||
return <Tag color="gold">{text}</Tag>
|
||||
}
|
||||
return <Tag color="red">{text}</Tag>
|
||||
}}
|
||||
/>
|
||||
<Column title="Groups" dataIndex="groupsCount" align="center"
|
||||
render={(text, record: UserDataTable, index) => {
|
||||
return renderPopoverGroups(text, record.auto_groups, record)
|
||||
}}
|
||||
/>
|
||||
<Column title="Role" dataIndex="role"
|
||||
onFilter={(value: string | number | boolean, record) => (record as any).role.includes(value)}
|
||||
sorter={(a, b) => ((a as any).role.localeCompare((b as any).role))}/>
|
||||
<Column title="" align="center" width="30px"
|
||||
render={(text, record, index) => {
|
||||
return (
|
||||
<Dropdown.Button type="text" overlay={actionsMenu}
|
||||
trigger={["click"]}
|
||||
onVisibleChange={visible => {
|
||||
if (visible) setUserToAction(record as UserDataTable)
|
||||
}}></Dropdown.Button>)
|
||||
}}
|
||||
/>
|
||||
</Table>
|
||||
</Card>
|
||||
</Space>
|
||||
<Tabs
|
||||
defaultActiveKey={tab}
|
||||
items={tabItems}
|
||||
onTabClick={onTabClick}
|
||||
animated={{ inkBar: true, tabPane: false }}
|
||||
tabPosition="top"
|
||||
// destroyInactiveTabPane={true}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Row>}
|
||||
{user && <UserEdit/>}
|
||||
</Container>
|
||||
<UserUpdate/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user