Compare commits

...
Author SHA1 Message Date
mlsmaycon 06ff449a1c Force service worker uninstall 2022-08-02 17:47:06 +02:00
mlsmaycon e1d12b91a2 Force service worker uninstall 2022-08-02 17:27:11 +02:00
Maycon Santos 54cd75301c Add rename peer (#59)
Added View option in the menu

Click on name to open the peer View option
2022-07-14 10:37:33 +02:00
braginini 1623dd243a Add access control banner 2022-06-24 22:43:23 +02:00
Misha Bragin cf0abd3029 Enable/Disable SSH server in the peers table (#54) 2022-06-23 17:01:40 +02:00
Misha Bragin 5bc913ff74 Make footer sticky (#56)
* Make footer sticky

* Fix footer positioning
2022-06-22 15:15:13 +02:00
Sang Woo Bae 0260dd5ce3 Link to documentation in installpage (#57)
* Link to documentation in installpage

* Add Documentation link for other distros

* Fix OS autodetection

* Reorder AddPeer tabs and add other tab
2022-06-22 14:36:40 +02:00
Maycon Santos aad85a1f67 Validate tag input and handle popover for 5+ groups (#55)
Validate tag naming input

check if only spaces has been added
check if group All is being removed
group All is not closeable in peers

Fixed issue when group list is too large, 
preventing click on the group counter

removed arrows from rule inputs
2022-06-20 17:07:59 +02:00
6724e5bfaa Feature/access control (#49)
Add Access control feature page

Users can create and update access control rules from the UI

Users can assign groups to peers

Minor enhancements to the UI

Co-authored-by: Raphael Oliveira <raphael.oliveira@dataontabs.com>
Co-authored-by: DataOnTabs <68952619+dataontabs@users.noreply.github.com>
Co-authored-by: mlsmaycon <mlsmaycon@gmail.com>
2022-06-15 11:42:15 +02:00
braginini 4e78fabb35 Update images size in README 2022-06-12 09:07:53 +02:00
braginini c4d679a371 Update readme and media 2022-06-12 09:06:47 +02:00
braginini 48d25a0008 Correct wording for popups 2022-06-08 12:15:04 +02:00
braginini a65b892441 Fix windows download 2022-06-08 00:19:03 +02:00
braginini 32e06013e9 Fix Try Again btn on auth error 2022-06-07 18:01:58 +02:00
braginini 0334899719 Add logout button on error 2022-06-07 16:26:16 +02:00
braginini 48e6041fd6 Don't log out when email not confirmed 2022-06-07 14:41:58 +02:00
braginini 72e24b8330 Add custom page for auth errors 2022-06-07 12:05:29 +02:00
Misha Bragin 665e6191ea Dynamically fetch the latest release of NetBird on startup (#52) 2022-06-06 13:44:18 +02:00
braginini e62e8a00f8 Detect OS to select installation Tab 2022-06-05 16:35:03 +02:00
45 changed files with 1480 additions and 482 deletions

No files matched your search

+18 -20
View File
@@ -1,37 +1,35 @@
# NetBird dashboard
### Big News! Wiretrustee becomes `NetBird`. [See details](https://blog.netbird.io/wiretrustee-becomes-netbird).
\
This project is the UI for NetBird's management service.
This project is the UI for NetBird's Management service.
**Hosted demo version:** https://app.netbird.io/
**Hosted version:** https://app.netbird.io/
See [NetBird repo](https://github.com/netbirdio/netbird)
## Why UI dashboard is needed?
## Why?
The purpose of this project is simple - make it easy to manage VPN built with [NetBird](https://github.com/netbirdio/netbird).
The dashboard makes it possible to:
- track the status of your peers
- remove Peers
- remove peers
- manage Setup Keys (to authenticate new peers)
- monitor system activity (coming soon)
- manage peer access (coming soon)
- list users
- define access controls
## Some Screenshots
<img src="./media/auth.png" alt="auth" width="350"/>
<img src="./media/peers.png" alt="peers" width="350"/>
<img src="./media/add-peer.png" alt="add-peer" width="350"/>
<img src="./media/auth.png" alt="auth"/>
<img src="./media/peers.png" alt="peers"/>
<img src="./media/add-peer.png" alt="add-peer"/>
## Technologies Used
- ReactJS
- Tailwind CSS. We are using paid License of [TailwindUI](https://tailwindui.com/)
- AntD UI framework
- Auth0
- Nginx
- Nginx
- Docker
- Letsencrypt
- Let's Encrypt
## How to run
Disclaimer. We believe that proper user management system is not a trivial task and requires quite some effort to make it right. Therefore we decided to
@@ -40,18 +38,18 @@ 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):
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```
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"
```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.
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```
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```
## How to run local development
+6 -1
View File
@@ -27,8 +27,13 @@ AUTH0_AUDIENCE=${AUTH0_AUDIENCE}
NETBIRD_MGMT_API_ENDPOINT=${NETBIRD_MGMT_API_ENDPOINT}
NETBIRD_MGMT_GRPC_API_ENDPOINT=${NETBIRD_MGMT_GRPC_API_ENDPOINT}
REPO="https://github.com/netbirdio/netbird/"
# this command will fetch the latest release e.g. v0.6.3
export NETBIRD_LATEST_VERSION=$(basename $(curl -fs -o/dev/null -w %{redirect_url} ${REPO}releases/latest))
echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"
# replace ENVs in the config
ENV_STR="\$\$AUTH0_DOMAIN \$\$AUTH0_CLIENT_ID \$\$AUTH0_AUDIENCE \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT"
ENV_STR="\$\$AUTH0_DOMAIN \$\$AUTH0_CLIENT_ID \$\$AUTH0_AUDIENCE \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_LATEST_VERSION"
MAIN_JS=$(find /usr/share/nginx/html/static/js/main.*js)
cp "$MAIN_JS" "$MAIN_JS".copy
envsubst "$ENV_STR" < "$MAIN_JS".copy > "$MAIN_JS"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 44 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 61 KiB

+37 -13
View File
@@ -1,6 +1,6 @@
import React, {useEffect, useState} from 'react';
import {Provider} from "react-redux";
import {Redirect, Route, Switch} from 'react-router-dom';
import {Link, Redirect, Route, Switch} from 'react-router-dom';
import {useAuth0} from "@auth0/auth0-react";
import Navbar from './components/Navbar';
import Peers from './views/Peers';
@@ -8,16 +8,16 @@ import FooterComponent from './components/FooterComponent';
import Loading from "./components/Loading";
import SetupKeys from "./views/SetupKeys";
import AddPeer from "./views/AddPeer";
import AccessControl from "./views/AccessControl";
import Activity from "./views/Activity";
import Users from './views/Users';
import AccessControl from './views/AccessControl';
// import Activity from './views/Activity';
import Banner from "./components/Banner";
import {store} from "./store";
import {Col, Layout, Row} from 'antd';
import { Container } from "./components/Container";
import {Button, Col, Layout, Result, Row} from 'antd';
import {Container} from "./components/Container";
const { Header, Content } = Layout;
const {Header, Content} = Layout;
function App() {
@@ -25,6 +25,7 @@ function App() {
isLoading,
isAuthenticated,
loginWithRedirect,
logout,
error
} = useAuth0();
@@ -50,7 +51,25 @@ function App() {
});
if (error) {
return <div>Oops... {error.message}</div>;
return <Result
status="warning"
title={error.message}
extra={<>
<a href={window.location.origin}>
<Button type="primary">
Try again
</Button>
</a>
<Button type="primary" onClick={function () {
logout({
returnTo: window.location.origin,
})
}}>
Log out
</Button>
</>
}
/>
}
if (isLoading) {
@@ -63,10 +82,15 @@ function App() {
return (
<Provider store={store}>
{ isAuthenticated &&
{isAuthenticated &&
<Layout>
<Banner/>
<Header className="header" style={{display: "flex", flexDirection: "column", justifyContent: "space-around", alignContent: "center"}}>
<Header className="header" style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-around",
alignContent: "center"
}}>
<Row justify="space-around" align="middle">
<Col span={24}>
<Container>
@@ -75,7 +99,7 @@ function App() {
</Col>
</Row>
</Header>
<Content>
<Content style={{ minHeight: "100vh"}}>
<Switch>
<Route
exact
@@ -89,14 +113,14 @@ function App() {
<Route path='/peers' exact component={Peers}/>
<Route path="/add-peer" component={AddPeer}/>
<Route path="/setup-keys" component={SetupKeys}/>
{/*<Route path="/acls" component={AccessControl}/>
<Route path="/activity" component={Activity}/>*/}
<Route path="/acls" component={AccessControl}/>
{/*<Route path="/activity" component={Activity}/>*/}
<Route path="/users" component={Users}/>
</Switch>
</Content>
<FooterComponent/>
</Layout>
}
}
</Provider>
);
}
+3 -3
View File
@@ -27,9 +27,9 @@ const AccessControlModalGroups:React.FC<Props> = ({data, title, visible, onCance
renderItem={(item:Group) => (
<List.Item>
<List.Item.Meta
avatar={<Avatar>{item.Name.slice(0,1).toUpperCase()}</Avatar>}
title={item.Name}
description={`${item.PeersCount} peers`}
avatar={<Avatar>{item.name.slice(0,1).toUpperCase()}</Avatar>}
title={item.name}
description={`${item.peers_count} peers`}
/>
</List.Item>
)}
+146 -37
View File
@@ -2,24 +2,22 @@ import React, {useEffect, useRef, useState} from 'react';
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "typesafe-actions";
import { actions as ruleActions } from '../store/rule';
import { actions as groupsActions } from '../store/group';
import inbound from '../assets/direct_in.svg';
import outbound from '../assets/direct_out.svg';
import {
Col,
Row,
Typography,
Input,
Space,
Radio,
Button, Drawer, Form, List, Divider, Select, Tag
Switch,
Button, Drawer, Form, Divider, Select, Tag, Radio, RadioChangeEvent
} from "antd";
import {ArrowRightOutlined, CloseOutlined, FlagFilled, QuestionCircleFilled} from "@ant-design/icons";
import {ArrowRightOutlined, CheckOutlined, CloseOutlined, FlagFilled, QuestionCircleFilled} from "@ant-design/icons";
import type { CustomTagProps } from 'rc-select/lib/BaseSelect'
import {Rule, RuleToSave} from "../store/rule/types";
import {useAuth0} from "@auth0/auth0-react";
import { uniq } from "lodash"
import {Header} from "antd/es/layout/layout";
import {RuleObject} from "antd/lib/form";
const { Paragraph } = Typography;
const { Option } = Select;
@@ -38,10 +36,14 @@ const AccessControlNew = () => {
const savedRule = useSelector((state: RootState) => state.rule.savedRule)
const [editName, setEditName] = useState(false)
const [editDescription, setEditDescription] = useState(false)
const [tagGroups, setTagGroups] = useState([] as string[])
const [formRule, setFormRule] = useState({} as FormRule)
const [form] = Form.useForm()
const inputNameRef = useRef<any>(null)
const inputDescriptionRef = useRef<any>(null)
const optionsDisabledEnabled = [{label: 'Enabled', value: false}, {label: 'Disabled', value: true}]
useEffect(() => {
if (editName) inputNameRef.current!.focus({
@@ -49,36 +51,44 @@ const AccessControlNew = () => {
});
}, [editName]);
useEffect(() => {
if (editDescription) inputDescriptionRef.current!.focus({
cursor: 'end',
});
}, [editDescription]);
useEffect(() => {
if (!rule) return
const fRule = {
...rule,
tagSourceGroups: rule.Source ? rule.Source?.map(t => t.Name) : [],
tagDestinationGroups: rule.Destination ? rule.Destination?.map(t => t.Name) : []
tagSourceGroups: rule.sources ? rule.sources?.map(t => t.name) : [],
tagDestinationGroups: rule.destinations ? rule.destinations?.map(t => t.name) : []
} as FormRule
setFormRule(fRule)
form.setFieldsValue(fRule)
}, [rule])
useEffect(() => {
setTagGroups(groups?.map(g => g.Name) || [])
setTagGroups(groups?.map(g => g.name) || [])
}, [groups])
const createRuleToSave = ():RuleToSave => {
const Source = groups?.filter(g => formRule.tagSourceGroups.includes(g.Name)).map(g => g.ID || '') || []
const Destination = groups?.filter(g => formRule.tagDestinationGroups.includes(g.Name)).map(g => g.ID || '') || []
const sources = groups?.filter(g => formRule.tagSourceGroups.includes(g.name)).map(g => g.id || '') || []
const destinations = groups?.filter(g => formRule.tagDestinationGroups.includes(g.name)).map(g => g.id || '') || []
const sourcesNoId = formRule.tagSourceGroups.filter(s => !tagGroups.includes(s))
const destinationsNoId = formRule.tagDestinationGroups.filter(s => !tagGroups.includes(s))
const groupsToSave = uniq([...sourcesNoId, ...destinationsNoId])
return {
ID: formRule.ID,
Name: formRule.Name,
Source,
Destination,
id: formRule.id,
name: formRule.name,
description: formRule.description,
sources,
destinations,
sourcesNoId,
destinationsNoId,
groupsToSave,
Flow: formRule.Flow
flow: formRule.flow,
disabled: formRule.disabled
} as RuleToSave
}
@@ -101,10 +111,12 @@ const AccessControlNew = () => {
if (savedRule.loading) return
setEditName(false)
dispatch(ruleActions.setRule({
Name: '',
Source: [],
Destination: [],
Flow: 'bidirect'
name: '',
description: '',
sources: [],
destinations: [],
flow: 'bidirect',
disabled: false
} as Rule))
setVisibleNewRule(false)
}
@@ -127,6 +139,13 @@ const AccessControlNew = () => {
})
};
const handleChangeDisabled = ({ target: { value } }: RadioChangeEvent) => {
setFormRule({
...formRule,
disabled: value
})
};
const tagRender = (props: CustomTagProps) => {
const { label, value, closable, onClose } = props;
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
@@ -149,8 +168,8 @@ const AccessControlNew = () => {
const optionRender = (label: string) => {
let peersCount = ''
const g = groups.find(_g => _g.Name === label)
if (g) peersCount = ` - ${g.PeersCount || 0} ${(g.PeersCount && parseInt(g.PeersCount) > 1) ? 'peers' : 'peer'} `
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
@@ -164,16 +183,56 @@ const AccessControlNew = () => {
)
}
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 toggleEditName = (status:boolean) => {
setEditName(status);
}
const toggleEditDescription = (status:boolean) => {
setEditDescription(status);
}
// const testDeleteGroup = () => {
// groups.forEach(g => {
// dispatch(groupsActions.deleteGroup.request({getAccessTokenSilently, payload: g.ID || ''}))
// })
// }
const selectValidator = (_: RuleObject, value: string[]) => {
let hasSpaceNamed = []
if (!value.length) {
return Promise.reject(new Error("Please enter ate least one group"))
}
value.forEach(function(v: string) {
if (!v.trim().length) {
hasSpaceNamed.push(v)
}
})
if (hasSpaceNamed.length) {
return Promise.reject(new Error("Group names with just spaces are not allowed"))
}
return Promise.resolve()
}
return (
<>
{rule &&
@@ -185,10 +244,11 @@ const AccessControlNew = () => {
visible={setupNewRuleVisible}
bodyStyle={{paddingBottom: 80}}
onClose={onCancel}
autoFocus={true}
footer={
<Space style={{display: 'flex', justifyContent: 'end'}}>
<Button onClick={onCancel} disabled={savedRule.loading}>Cancel</Button>
<Button type="primary" disabled={savedRule.loading} onClick={handleFormSubmit}>{`${formRule.ID ? 'Save' : 'Create'}`}</Button>
<Button type="primary" disabled={savedRule.loading} onClick={handleFormSubmit}>{`${formRule.id ? 'Save' : 'Create'}`}</Button>
</Space>
}
>
@@ -198,7 +258,7 @@ const AccessControlNew = () => {
<Header style={{margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px"}}>
<Row align="top">
<Col flex="none" style={{display: "flex"}}>
{!editName && formRule.ID &&
{!editName && !editDescription && formRule.id &&
<button type="button" aria-label="Close" className="ant-drawer-close"
style={{paddingTop: 3}}
onClick={onCancel}>
@@ -209,32 +269,76 @@ const AccessControlNew = () => {
}
</Col>
<Col flex="auto">
{ !editName && formRule.ID ? (
<div className={"access-control ant-drawer-title"} onClick={() => toggleEditName(true)}>{formRule.ID ? formRule.Name : 'New Rule'}</div>
{ !editName && formRule.id ? (
<div className={"access-control input-text ant-drawer-title"} onClick={() => toggleEditName(true)}>{formRule.id ? formRule.name : 'New Rule'}</div>
) : (
<Form.Item
name="Name"
label={null}
rules={[{required: true, message: 'Please add a name for this access rule'}]}
name="name"
label="Name"
rules={[{required: true, message: 'Please add a name for this access rule', whitespace: true}]}
>
<Input placeholder="Add rule name..." ref={inputNameRef} onPressEnter={() => toggleEditName(false)} onBlur={() => toggleEditName(false)} autoComplete="off"/>
</Form.Item>
)}
{ !editDescription ? (
<div className={"access-control input-text ant-drawer-subtitle"} onClick={() => toggleEditDescription(true)}>{formRule.description && formRule.description.trim() !== "" ? formRule.description : 'Add description...'}</div>
) : (
<Form.Item
name="description"
label="Description"
style={{marginTop: 24}}
>
<Input placeholder="Add description..." ref={inputDescriptionRef} onPressEnter={() => toggleEditDescription(false)} onBlur={() => toggleEditDescription(false)} autoComplete="off"/>
</Form.Item>
)}
</Col>
</Row>
<Row align="top">
<Col flex="auto">
</Col>
</Row>
</Header>
</Col>
<Col span={24}>
</Col>
<Col span={24}>
<Form.Item
name="disabled"
label="Status"
//valuePropName="checked"
>
{/*<Switch
checkedChildren={<CheckOutlined />}
unCheckedChildren={<CloseOutlined />}
onChange={handleChangeDisabled}
/>*/}
<Radio.Group
options={optionsDisabledEnabled}
onChange={handleChangeDisabled}
optionType="button"
buttonStyle="solid"
/>
</Form.Item>
</Col>
<Col span={24}>
<Form.Item
name="tagSourceGroups"
label={<>Source groups&nbsp;<ArrowRightOutlined /></>}
rules={[{required: true, message: 'Please enter ate least one group'}]}
label="Source groups"
rules={[{ validator: selectValidator }]}
style={{display: 'flex'}}
>
<Select mode="tags" style={{ width: '100%' }} placeholder="Tags Mode" tagRender={tagRender} onChange={handleChangeSource}>
<Select mode="tags"
style={{ width: '100%' }}
placeholder="Tags Mode"
tagRender={tagRender}
onChange={handleChangeSource}
dropdownRender={dropDownRender}
>
{
tagGroups.map(m =>
<Option key={m}>{optionRender(m)}</Option>
@@ -246,11 +350,17 @@ const AccessControlNew = () => {
<Col span={24}>
<Form.Item
name="tagDestinationGroups"
label={<><ArrowRightOutlined />&nbsp;Destination groups</>}
rules={[{required: true, message: 'Please enter ate least one group'}]}
label="Destination groups"
rules={[{ validator: selectValidator }]}
style={{display: 'flex'}}
>
<Select mode="tags" style={{ width: '100%' }} placeholder="Tags Mode" tagRender={tagRender} onChange={handleChangeDestination}>
<Select
mode="tags" style={{ width: '100%' }}
placeholder="Tags Mode"
tagRender={tagRender}
onChange={handleChangeDestination}
dropdownRender={dropDownRender}
>
{
tagGroups.map(m =>
<Option key={m}>{optionRender(m)}</Option>
@@ -271,7 +381,6 @@ const AccessControlNew = () => {
<Paragraph>
If you want to enable all peers of the same group to talk to each other - you can add that group both as a receiver and as a destination.
</Paragraph>
<a style={{color: 'rgb(07, 114, 128)'}} href="https://docs.netbird.io/overview/access-control" target="_blank">Learn more about access control...</a>
</Col>
</Row>
</Col>
@@ -279,7 +388,7 @@ const AccessControlNew = () => {
<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 setup keys</Button>
more about access controls</Button>
</Col>
</Row>
</Form>
+3 -3
View File
@@ -14,7 +14,7 @@ const Banner = () => {
const linkLearnMore = () => {
return (
<a
href="https://blog.netbird.io/wiretrustee-becomes-netbird"
href="https://netbird.io/blog/introducing-access-control"
className="font-bold underline"
target="_blank"
rel="noreferrer"
@@ -27,7 +27,7 @@ const Banner = () => {
<Row>
<Col xs={24} sm={0} lg={0}>
<Text className="ant-col-md-0" style={{color: "#ffffff"}}>
Big news! Wiretrustee becomes <strong>NetBird</strong>!
Big news! Introducing NetBird Access Control.
</Text>
</Col>
<Col xs={24} sm={0} lg={0}>
@@ -38,7 +38,7 @@ const Banner = () => {
<Col xs={0} sm={24}>
<Space align="center" style={{display: "flex", justifyContent: "center"}}>
<Text style={{color: "#ffffff"}}>
Big news! Wiretrustee becomes <strong>NetBird</strong>!
Big news! Introducing NetBird Access Control.
</Text>
<span>
{linkLearnMore()}
+1 -2
View File
@@ -1,12 +1,11 @@
import React from 'react';
import { Layout } from 'antd';
import {Link} from 'react-router-dom';
const { Footer } = Layout
export default () => {
return (
<Footer style={{ textAlign: 'center' }}>
<Footer style={{ textAlign: 'center', bottom: "0"}}>
Copyright © 2022 <a href="https://netbird.io">NetBird Authors</a>
</Footer>
);
+4 -4
View File
@@ -5,8 +5,8 @@ import {useAuth0} from "@auth0/auth0-react";
import {useLocation} from 'react-router-dom';
import {Menu, Row, Col, Grid, Dropdown, Avatar, Button, Typography, Space} from 'antd'
import {ItemType} from "antd/lib/menu/hooks/useItems";
import {UserOutlined} from "@ant-design/icons";
import {AvatarSize} from "antd/es/avatar/SizeContext";
import { UserOutlined } from '@ant-design/icons';
const { Text } = Typography
const { useBreakpoint } = Grid;
@@ -30,8 +30,8 @@ const Navbar = () => {
{ 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="/activity">Activity</Link>), key: '/activity' },*/
{ label: (<Link to="/acls">Access Control</Link>), key: '/acls' },
// { label: (<Link to="/activity">Activity</Link>), key: '/activity' },
{ label: (<Link to="/users">Users</Link>), key: '/users' }
] as ItemType[])
@@ -80,7 +80,7 @@ const Navbar = () => {
const createAvatar = (size:AvatarSize) => {
return user?.picture ? (
<Avatar size={size} src={user?.picture}/>
<Avatar size={size} src={user?.picture} icon={<UserOutlined />} />
) : (
<Avatar size={size}>{(user?.name || '').slice(0, 1).toUpperCase()}</Avatar>
)
+368
View File
@@ -0,0 +1,368 @@
import React, {useEffect, useRef, useState} from 'react';
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "typesafe-actions";
import {actions as peerActions} from '../store/peer';
import {Button, Col, Divider, Drawer, Form, Input, Row, Select, Space, Tag, Typography} from "antd";
import {Header} from "antd/es/layout/layout";
import type {CustomTagProps} from 'rc-select/lib/BaseSelect'
import {useAuth0} from "@auth0/auth0-react";
import {Peer, PeerGroupsToSave} from "../store/peer/types";
import {Group, GroupPeer} from "../store/group/types";
import {CloseOutlined, EditOutlined, FlagFilled} from "@ant-design/icons";
import {RuleObject} from 'antd/lib/form';
const { Paragraph } = Typography;
const { Option } = Select;
const PeerUpdate = () => {
const { getAccessTokenSilently } = useAuth0()
const dispatch = useDispatch()
const groups = useSelector((state: RootState) => state.group.data)
const peer = useSelector((state: RootState) => state.peer.peer)
const [formPeer, setFormPeer] = useState({} as Peer)
const updateGroupsVisible = useSelector((state: RootState) => state.peer.updateGroupsVisible)
const savedGroups = useSelector((state: RootState) => state.peer.savedGroups)
const updatedPeers = useSelector((state: RootState) => state.peer.updatedPeer)
const [tagGroups, setTagGroups] = useState([] as string[])
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[])
const [peerGroups, setPeerGroups] = useState([] as GroupPeer[])
const inputNameRef = useRef<any>(null)
const [editName, setEditName] = useState(false)
const [callingPeerAPI, setCallingPeerAPI] = useState(false)
const [callingGroupAPI, setCallingGroupAPI] = useState(false)
const [isSubmitRunning, setSubmitRunning] = useState(false)
const [peerGroupsToSave, setPeerGroupsToSave] = useState({
ID: '',
groupsNoId: [],
groupsToSave: [],
groupsToRemove: [],
groupsToAdd: []
} as PeerGroupsToSave)
const [form] = Form.useForm()
// wait peer update to succeed
useEffect(() => {
if (callingPeerAPI && updatedPeers.success) {
setCallingPeerAPI(false)
}
},[updatedPeers])
// wait save groups to succeed
useEffect(() => {
if (callingGroupAPI && savedGroups.success) {
setCallingGroupAPI(false)
}
},[savedGroups])
// clean temp state and close
useEffect(() => {
if (isSubmitRunning && !callingGroupAPI && !callingPeerAPI) {
onCancel()
}
},[callingGroupAPI,callingPeerAPI])
useEffect(() => {
if (editName) inputNameRef.current!.focus({
cursor: 'end',
});
}, [editName]);
useEffect(() => {
if (!peer) return
const gs = peer?.groups?.map(g => ({id: g?.id || '', name: g.name} as GroupPeer)) as GroupPeer[]
const gs_name = gs?.map(g => g.name) as string[]
setPeerGroups(gs)
setSelectedTagGroups(gs_name)
setFormPeer(peer)
form.setFieldsValue({
name: formPeer.name ? formPeer.name: peer.name,
groups: gs_name
})
}, [peer])
useEffect(() => {
setTagGroups(groups?.map(g => g.name) || [])
}, [groups])
const toggleEditName = (status:boolean) => {
setEditName(status)
}
useEffect(() => {
const groupsToRemove = peerGroups.filter(pg => !selectedTagGroups.includes(pg.name)).map(g => g.id)
const groupsToAdd = (groups as Group[]).filter(g => selectedTagGroups.includes(g.name) && !groupsToRemove.includes(g.id || '') && !peerGroups.find(pg => pg.id === g.id)).map(g => g.id) as string[]
const groupsNoId = selectedTagGroups.filter(stg => !groups.find(g => g.name === stg))
setPeerGroupsToSave({
...peerGroupsToSave,
ID: peer?.id || '',
groupsToRemove,
groupsToAdd,
groupsNoId
})
}, [selectedTagGroups])
const tagRender = (props: CustomTagProps) => {
const { label, value, closable, onClose } = props;
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
event.preventDefault();
event.stopPropagation();
};
let tagClosable = true
if (value === "All") {
tagClosable = false
}
return (
<Tag
color="blue"
onMouseDown={onPreventMouseDown}
closable={tagClosable}
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 setUpdateGroupsVisible = (status:boolean) => {
dispatch(peerActions.setUpdateGroupsVisible(status));
}
const onCancel = () => {
dispatch(peerActions.setPeer(null))
setUpdateGroupsVisible(false)
setEditName(false)
// setSaveBtnDisabled(true)
setFormPeer({} as Peer)
setCallingPeerAPI(false)
setCallingPeerAPI(false)
setSubmitRunning(false)
}
const noUpdateToGroups = ():Boolean => {
return !peerGroupsToSave.groupsToRemove.length && !peerGroupsToSave.groupsToAdd.length && !peerGroupsToSave.groupsNoId.length
}
const noUpdateToName = ():Boolean => {
return !formPeer.name || formPeer.name === peer.name
}
const onChange = (data:any) => {
setFormPeer({...formPeer, ...data})
}
const handleChangeTags = (value: string[]) => {
let validatedValues: string[] = []
value.forEach(function(v) {
if (v.trim().length) {
validatedValues.push(v)
}
})
setSelectedTagGroups(validatedValues)
};
const createPeerToSave = ():Peer => {
return {
id: formPeer.id,
ssh_enabled: formPeer.ssh_enabled,
name: formPeer.name
} as Peer
}
const handleFormSubmit = () => {
form.validateFields()
.then((values) => {
setSubmitRunning(true)
if (!noUpdateToName()) {
const peerUpdate = createPeerToSave()
setCallingPeerAPI(true)
dispatch(peerActions.updatePeer.request({getAccessTokenSilently, payload: peerUpdate}))
}
if (peerGroupsToSave.groupsToRemove.length || peerGroupsToSave.groupsToAdd.length || peerGroupsToSave.groupsNoId.length) {
setCallingGroupAPI(true)
dispatch(peerActions.saveGroups.request({getAccessTokenSilently, payload: peerGroupsToSave}))
}
})
.catch((errorInfo) => {
console.log('errorInfo', errorInfo)
});
}
const selectValidator = (_: RuleObject, value: string[]) => {
let hasSpaceNamed = []
let isAllPresent = false
if (!value.length) {
return Promise.reject(new Error("Please enter ate least one group"))
}
value.forEach(function(v: string) {
if (!v.trim().length) {
hasSpaceNamed.push(v)
}
if (v === 'All') {
isAllPresent = true
}
})
if (!isAllPresent) {
return Promise.reject(new Error("The All group can't be removed"))
}
if (hasSpaceNamed.length) {
return Promise.reject(new Error("Group names with just spaces are not allowed"))
}
return Promise.resolve()
}
return (
<>
{peer &&
<Drawer
forceRender={true}
headerStyle={{display: "none"}}
visible={true}
bodyStyle={{paddingBottom: 80}}
onClose={onCancel}
autoFocus={true}
footer={
<Space style={{display: 'flex', justifyContent: 'end'}}>
<Button onClick={onCancel} disabled={savedGroups.loading}>Cancel</Button>
<Button type="primary" disabled={(savedGroups.loading || updatedPeers.loading) || (noUpdateToGroups() && noUpdateToName())} onClick={handleFormSubmit}>Save</Button>
</Space>
}
>
<Form layout="vertical" hideRequiredMark form={form} onValuesChange={onChange}>
<Row gutter={16}>
<Col span={24}>
<Header style={{margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px"}}>
<Row align="top">
<Col flex="none" style={{display: "flex"}}>
{!editName && peer.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 && peer.id && formPeer.name ? (
<div className={"access-control input-text ant-drawer-title"}
onClick={() => toggleEditName(true)}>{formPeer.name ? formPeer.name : peer.name}
<EditOutlined/></div>
) : (
<Form.Item
name="name"
label="Update Name"
rules={[{
required: true,
message: 'Please add a new name for this peer',
whitespace: true
}]}
style={{display: 'flex'}}
>
<Input
placeholder={peer.name}
ref={inputNameRef}
onPressEnter={() => toggleEditName(false)}
onBlur={() => toggleEditName(false)}
// onChange={(e) => handleChangeName(e.)}
autoComplete="off"/>
</Form.Item>)}
</Col>
</Row>
</Header>
</Col>
</Row>
<Row gutter={16}>
<Col span={24}>
<Form.Item
name="groups"
label="Select groups to associate with this peer"
rules={[{ validator: selectValidator }]}
style={{display: 'flex'}}
>
<Select
mode="tags"
style={{ width: '100%' }}
placeholder="Select groups..."
tagRender={tagRender}
// onDropdownVisibleChange={evaluateSubmit}
// onSelect={evaluateSubmit}
dropdownRender={dropDownRender}
onChange={handleChangeTags}>
{
tagGroups.map(m =>
<Option key={m}>{optionRender(m)}</Option>
)
}
</Select>
</Form.Item>
</Col>
<Col span={24}>
<Row wrap={false} gutter={12}>
<Col flex="none">
<FlagFilled/>
</Col>
<Col flex="auto">
<Paragraph>
Every peer is part of the group All, thus you can't remove it.
</Paragraph>
</Col>
</Row>
</Col>
</Row>
</Form>
</Drawer>
}
</>
)
}
export default PeerUpdate
+2 -2
View File
@@ -48,8 +48,8 @@ const SetupKeyNew = () => {
const onCancel = () => {
if (createdSetupKey.loading) return
dispatch(setupKeyActions.setSetupKey({
Name: '',
Type: 'reusable'
name: '',
type: 'reusable'
} as SetupKey))
setVisibleNewSetupKey(false)
}
+12
View File
@@ -0,0 +1,12 @@
import React from "react";
import {SpinProps} from "antd";
const TableSpin = (loading: boolean): SpinProps => {
return {
spinning: loading,
delay: 1,
size: "large"
}
}
export default TableSpin;
+11 -64
View File
@@ -1,81 +1,28 @@
import React, {useState} from 'react';
import { useState } from 'react';
import { Button } from "antd";
import TabSteps from "./TabSteps";
import { StepCommand } from "./types"
import {getConfig} from "../../config";
const {grpcApiOrigin} = getConfig();
export const LinuxTab = () => {
export const OtherTab = () => {
const formatNetBirdUP = () => {
let cmd = "sudo netbird up"
if (grpcApiOrigin) {
cmd = "sudo netbird up --management-url " + grpcApiOrigin
}
return [
cmd
].join('\n')
}
const [steps, setSteps] = useState([
const [steps, _] = useState([
{
key: 1,
title: 'Add repository:',
commands: [
`sudo apt install ca-certificates curl gnupg -y`,
`curl -L https://pkgs.wiretrustee.com/debian/public.key | sudo apt-key add -`,
`echo 'deb https://pkgs.wiretrustee.com/debian stable main' | sudo tee /etc/apt/sources.list.d/wiretrustee.list`
].join('\n'),
title: 'For other installation options check our documentation.',
commands: (
<Button type="primary" href={`https://netbird.io/docs/getting-started/installation#binary-install`} target="_blank">
Documentation
</Button>
),
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}/>
<TabSteps stepsItems={steps} />
)
}
export default LinuxTab
export default OtherTab
+82
View File
@@ -0,0 +1,82 @@
import React, { useState } from 'react';
import { Button } from "antd";
import TabSteps from "./TabSteps";
import { StepCommand } from "./types"
import { getConfig } from "../../config";
import Paragraph from 'antd/lib/skeleton/Paragraph';
const { grpcApiOrigin } = getConfig();
export const UbuntuTab = () => {
const formatNetBirdUP = () => {
let cmd = "sudo netbird up"
if (grpcApiOrigin) {
cmd = "sudo netbird up --management-url " + grpcApiOrigin
}
return [
cmd
].join('\n')
}
const [steps, setSteps] = useState([
{
key: 1,
title: 'Add repository:',
commands: [
`sudo apt install ca-certificates curl gnupg -y`,
`curl -L https://pkgs.wiretrustee.com/debian/public.key | sudo apt-key add -`,
`echo 'deb https://pkgs.wiretrustee.com/debian stable main' | sudo tee /etc/apt/sources.list.d/wiretrustee.list`
].join('\n'),
copied: false,
showCopyButton: true
} as StepCommand,
{
key: 2,
title: 'Install NetBird:',
commands: [
`sudo apt-get update`,
`# for CLI only`,
`sudo apt-get install netbird`,
`# for GUI package`,
`sudo apt-get install netbird-ui`
].join('\n'),
copied: false,
showCopyButton: true
} as StepCommand,
{
key: 3,
title: 'Run NetBird and log in the browser:',
commands: formatNetBirdUP(),
copied: false,
showCopyButton: true
} as StepCommand,
{
key: 4,
title: 'Get your IP address:',
commands: [
`ip addr show wt0`
].join('\n'),
copied: false,
showCopyButton: true
} as StepCommand,
])
/*const clickTest = () => {
steps.push({
key: steps.length+1,
title: `Test ${steps.length+1}`,
commands: [`hi lorena!`].join('\n'),
copied: false
})
console.log(steps)
setSteps([...steps])
}*/
return (
<TabSteps stepsItems={steps} />
)
}
export default UbuntuTab
+3 -1
View File
@@ -3,10 +3,12 @@ import React, {useState} from 'react';
import { Button } from "antd";
import TabSteps from "./TabSteps";
import { StepCommand } from "./types"
import {getConfig} from "../../config";
const {latestVersion} = getConfig();
export const WindowsTab = () => {
const releaseVersion = '0.6.2'
const releaseVersion = latestVersion ? latestVersion.replace("v", "") : "0.6.3"
const [steps, setSteps] = useState([
{
key: 1,
+2 -1
View File
@@ -3,5 +3,6 @@
"clientId": "$AUTH0_CLIENT_ID",
"audience": "$AUTH0_AUDIENCE",
"apiOrigin": "$NETBIRD_MGMT_API_ENDPOINT",
"grpcApiOrigin": "$NETBIRD_MGMT_GRPC_API_ENDPOINT"
"grpcApiOrigin": "$NETBIRD_MGMT_GRPC_API_ENDPOINT",
"latestVersion": "$NETBIRD_LATEST_VERSION"
}
+1
View File
@@ -24,6 +24,7 @@ export function getConfig() {
clientId: configJson.clientId,
apiOrigin: configJson.apiOrigin,
grpcApiOrigin: configJson.grpcApiOrigin,
latestVersion: configJson.latestVersion,
...(audience ? { audience } : null),
};
}
+11 -1
View File
@@ -122,7 +122,17 @@ body {
color: rgba(0, 0, 0, .85) !important;
}
.access-control.ant-drawer-title:hover {
.access-control-table .tooltip-label:hover {
text-decoration: underline;
cursor: pointer;
}
.access-control.input-text:hover {
text-decoration: underline;
cursor: pointer;
}
.access-control.ant-drawer-subtitle {
line-height: 22px;
margin: 24px 0;
}
Loaded 20 of 45 files, more files were not shown because too many files have changed in this diff. Show more