Compare commits

...

24 Commits

Author SHA1 Message Date
Misha Bragin
5bd94eff56 Fix add peer popup tab layout on mobile (#154)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-03-23 12:10:27 +01:00
Misha Bragin
77f065b093 Update theme border radius (#152) 2023-03-22 16:56:36 +01:00
Misha Bragin
a4d55cfb90 Fix settings modal styles (#151) 2023-03-22 16:14:49 +01:00
Misha Bragin
cfd4c9075b Add onboarding steps (#150) 2023-03-22 15:21:52 +01:00
braginini
962180030a Fix activity docs link 2023-03-15 14:20:48 +01:00
Misha Bragin
485e1e8d79 Add more references to docs (#149) 2023-03-15 14:18:22 +01:00
Givi Khojanashvili
b11007b29f Add policy add activities (#147)
Related changes for netbirdio/netbird#700
2023-03-13 10:58:34 +01:00
Maycon Santos
bce75c1ca9 Disable banner (#146)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-03-02 15:55:31 +01:00
Maycon Santos
0c09992b38 Use user.is_current (#145)
prevent update for own user

use the is_current for label

removed unused imports
2023-03-01 22:50:26 +01:00
Maycon Santos
86b12f30d2 Use new windows release URL (#144)
removed latestVersion logic and environment support
2023-03-01 15:00:36 +01:00
Maycon Santos
d3e34d8448 Use single page size helper with new larger values (#143)
Reduce duplicated code with single helper for pagination

On larger deployments we should allow for larger pages

fix popover key issue and vertical spacing

update deprecated keys
2023-02-28 16:32:12 +01:00
Misha Bragin
76083168f6 Log error when activity event is not handled (#142) 2023-02-28 11:22:40 +01:00
Misha Bragin
a54b3687ae Display user in the user role update event (#141)
Handling of the "User role updated" event (user.role.update) 
was missing in the Activity tab.
2023-02-27 15:27:00 +01:00
Zoltan Papp
25f154dc83 Capitalize first letter of OS (#140) 2023-02-27 14:54:39 +01:00
pascal-fischer
f3c7d877f8 Add additional step in the Mac installation instructions to start the daemon (#138) 2023-02-22 20:55:36 +01:00
Misha Bragin
7cea7e7f54 Format individual peer login expiration event (#139)
Events peer.login.expiration.disable and peer.login.expiration.enable
are handled in the Activity tab now and properly displayed.
2023-02-22 20:54:56 +01:00
Misha Bragin
aaa351635f Add peer expiration setting confirmation modal (#137)
Add a confirmation dialog to notify a user of possible
consequences of the peer login expiration enabling/disabling.
2023-02-21 08:47:14 +01:00
Misha Bragin
379ff5486e Account settings view (#136)
New Settings tab added to the dashboard.
It is possible to enable or disable peer login expiration globally for an account.
As well as defining the expiration time period.
2023-02-20 08:57:24 +01:00
Misha Bragin
8bcd9918e2 Fix expires in input filed of the setup key (#135) 2023-02-17 14:30:22 +01:00
Misha Bragin
044ccd0ce6 Display login expiration activities (#134) 2023-02-16 15:36:45 +01:00
braginini
ab09ca3697 Display API error in peers view 2023-02-16 13:03:39 +01:00
Misha Bragin
1644ed5dce Add peer login expiration tooltip (#133) 2023-02-16 12:47:35 +01:00
Misha Bragin
cea459792f Add peer login expiration (#132)
Display if peer login has expired in the Peers table
Enable/Disable peer login expiration
2023-02-16 12:18:15 +01:00
Maycon Santos
a402680816 Disallow all crawlers and dashboard indexing (#131)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2023-02-10 18:45:11 +01:00
53 changed files with 1887 additions and 863 deletions

View File

@@ -53,13 +53,10 @@ export NETBIRD_MGMT_API_ENDPOINT=$(echo $NETBIRD_MGMT_API_ENDPOINT | sed -E 's/(
export NETBIRD_MGMT_GRPC_API_ENDPOINT=${NETBIRD_MGMT_GRPC_API_ENDPOINT}
export NETBIRD_HOTJAR_TRACK_ID=${NETBIRD_HOTJAR_TRACK_ID}
REPO="https://github.com/netbirdio/netbird/"
# this command will fetch the latest release e.g. v0.6.3
export NETBIRD_LATEST_VERSION=$(basename $(curl -fs -o/dev/null -w %{redirect_url} ${REPO}releases/latest))
echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"
# replace ENVs in the config
ENV_STR="\$\$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"
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

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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"

View File

@@ -1,3 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
Disallow: /

View File

@@ -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";
@@ -21,7 +21,7 @@ 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;
@@ -57,7 +57,7 @@ function App() {
run.current = true
apiClient.request<User[]>('GET', `/api/users`, {getAccessTokenSilently: getAccessTokenSilently})
.then(() => {
setShow(true)
setShow(true)
})
.catch(e => {
setShow(true)
@@ -69,50 +69,61 @@ function App() {
return (
<>
<ConfigProvider
theme={{
token: {
borderRadius: 4,
colorPrimary: "#1890ff",
fontFamily: "Arial"
},
}}
>
<Provider store={store}>
{!show && <SecureLoading padding="3em" width={50} height={50}/>}
{show &&
<Layout>
<Banner/>
<Header className="header" style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-around",
alignContent: "center"
}}>
<Row justify="space-around" align="middle">
<Col span={24}>
<Container>
<Navbar/>
</Container>
</Col>
</Row>
</Header>
<Content style={{minHeight: "100vh"}}>
<Switch>
<Route
exact
path="/"
render={() => {
return (
<Redirect to="/peers"/>
)
}}
/>
<Route path='/peers' exact component={withOidcSecure(Peers)}/>
<Route path="/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)}/>
<Route path="/activity" component={withOidcSecure(Activity)}/>
</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>
</>
)
}

View File

@@ -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}

View File

@@ -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/docs/overview/acls">Learn more about access controls</Button>
</Col>
</Row>
</Form>

View File

@@ -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');
}

View File

@@ -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://netbird.io/docs/how-to-guides/nameservers">Learn more about nameservers</Button>
</Col>
</Row>
</Form>) :

View File

@@ -35,19 +35,19 @@ 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="/activity">Activity</Link>), key: '/activity'}
{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", "/activity"]
const adminOnlyTabs = ["/setup-keys", "/acls", "/routes", "/dns", "/activity", "/settings"]
const [menuItems, setMenuItems] = useState(items)
const logoutWithRedirect = () =>
logout("/", {client_id: config.clientId});

View File

@@ -2,7 +2,7 @@ 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";
@@ -18,6 +18,7 @@ const {Panel} = Collapse;
const punycode = require('punycode/')
const PeerUpdate = () => {
const {getAccessTokenSilently} = useGetAccessTokenSilently()
const dispatch = useDispatch()
const groups = useSelector((state: RootState) => state.group.data)
@@ -200,6 +201,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 +237,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,7 +246,7 @@ const PeerUpdate = () => {
form.validateFields()
.then((values) => {
setSubmitRunning(true)
if (!noUpdateToName()) {
if (!noUpdateToName() || !noUpdateToLoginExpiration()) {
const peerUpdate = createPeerToSave()
setCallingPeerAPI(true)
dispatch(peerActions.updatePeer.request({
@@ -248,7 +254,7 @@ const PeerUpdate = () => {
payload: peerUpdate
}))
}
if (peerGroupsToSave.groupsToRemove.length || peerGroupsToSave.groupsToAdd.length || peerGroupsToSave.groupsNoId.length) {
if (!noUpdateToGroups()) {
setCallingGroupAPI(true)
dispatch(peerActions.saveGroups.request({
getAccessTokenSilently: getAccessTokenSilently,
@@ -303,7 +309,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>
}
@@ -435,6 +441,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 +478,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"}}>

View File

@@ -453,9 +453,7 @@ const RouteUpdate = () => {
<Col span={24}>
<Divider></Divider>
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
href="https://netbird.io/docs/how-to-guides/network-routes"
style={{color: 'rgb(07, 114, 128)'}}>Learn
more about network routes</Button>
href="https://netbird.io/docs/how-to-guides/network-routes">Learn more about network routes</Button>
</Col>
</Row>
</Form>

View File

@@ -11,7 +11,7 @@ import {
Form,
Input,
InputNumber,
List,
List, Modal,
Radio,
Row,
Select,
@@ -23,25 +23,27 @@ 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 {checkExpiresIn, formatDate, timeAgo} from "../utils/common";
import {RuleObject} from "antd/lib/form";
import {CustomTagProps} from "rc-select/lib/BaseSelect";
import {Group} from "../store/group/types";
import {useGetAccessTokenSilently} from "../utils/token";
import ExpiresInInput, {ExpiresInValue} from "../views/ExpiresInInput";
import ExpiresInInput, {expiresInToSeconds, ExpiresInValue} from "../views/ExpiresInInput";
import moment from "moment";
const {Option} = Select;
const {Text} = Typography;
const ExpiresInDefault: ExpiresInValue = {number: 30, interval: "Days"}
const ExpiresInDefault: ExpiresInValue = {number: 30, interval: "day"}
const customExpiresFormat: DatePickerProps['format'] = value => {
return formatDate(value)
}
const customLastUsedFormat: DatePickerProps['format'] = value => {
if (value.toString().startsWith("0001")) {
if (value.year() == 1) {
// 1st of Jan 0001
return "never"
}
let ago = timeAgo(value.toString())
@@ -93,7 +95,9 @@ const SetupKeyNew = () => {
const fSetupKey = {
...setupKey,
autoGroupNames: setupKey.auto_groups ? formKeyGroups : [],
expiresInFormatted: ExpiresInDefault
expiresInFormatted: ExpiresInDefault,
exp: moment(setupKey.expires),
last: moment(setupKey.last_used)
} as FormSetupKey
setFormSetupKey(fSetupKey)
form.setFieldsValue(fSetupKey)
@@ -117,31 +121,6 @@ const SetupKeyNew = () => {
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
}
return expiresIn.number * multiplier
}
const handleFormSubmit = () => {
form.validateFields()
@@ -290,13 +269,6 @@ const SetupKeyNew = () => {
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 &&
@@ -384,7 +356,7 @@ const SetupKeyNew = () => {
{setupKey.id && formSetupKey.name &&
<Col span={12}>
<Form.Item
name="expires"
name="exp"
label="Expires"
tooltip="The expiration date of the key"
>
@@ -397,7 +369,7 @@ const SetupKeyNew = () => {
{setupKey.id && formSetupKey.name &&
<Col span={12}>
<Form.Item
name="last_used"
name="last"
label="Last Used"
tooltip="The last time the key was used"
>
@@ -447,7 +419,12 @@ const SetupKeyNew = () => {
<Col span={24}>
<Form.Item name="expiresInFormatted" label="Expires In"
rules={[{validator: checkExpiresIn}]}>
<ExpiresInInput/>
<ExpiresInInput options={
Array.of(
{key: "day", title: "Days"},
{key: "month", title: "Months"},
{key: "year", title: "Years"})
}/>
</Form.Item>
</Col>}
<Col span={12}>
@@ -455,7 +432,8 @@ const SetupKeyNew = () => {
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"}
<InputNumber min={0} defaultValue={0}
disabled={setupKey.id || formSetupKey.type !== "reusable"}
style={{width: "100%"}}
/>
</Form.Item>
@@ -496,9 +474,7 @@ const SetupKeyNew = () => {
<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>
href="https://netbird.io/docs/overview/setup-keys">Learn more about setup keys</Button>
</Col>
</Row>
</Form>

View File

@@ -12,7 +12,7 @@ import {actions as userActions} from "../store/user";
import {useGetAccessTokenSilently} from "../utils/token";
import {useOidcUser} from "@axa-fr/react-oidc";
const {Paragraph, Text} = Typography;
const {Paragraph} = Typography;
const {confirm} = Modal;
@@ -358,7 +358,7 @@ const UserUpdate = () => {
>
<Select
style={{width: '100%'}}
disabled={currentUser.role != null && currentUser.role !== "admin"}>
disabled={(currentUser.role != null && currentUser.role !== "admin") || formUser.is_current}>
<Option value="admin">admin</Option>
<Option value="user">user</Option>
</Select>

View File

@@ -0,0 +1,100 @@
import React, {useState} from 'react';
import {Tabs, TabsProps} from "antd";
import Icon, {AndroidFilled, AppleFilled, WindowsFilled} from "@ant-design/icons";
import {ReactComponent as LinuxSVG} from "../icons/terminal_icon.svg";
import UbuntuTab from "./UbuntuTab";
import {ReactComponent as DockerSVG} from "../icons/docker_icon.svg";
import Paragraph from "antd/lib/typography/Paragraph";
import WindowsTab from "./WindowsTab";
import MacTab from "./MacTab";
import Link from "antd/lib/typography/Link";
import DockerTab from "./DockerTab";
type Props = {
greeting?: string;
headline: string;
};
const detectOSTab = () => {
let os = 1;
if (navigator.userAgent.indexOf("Win") !== -1) os = 2;
if (navigator.userAgent.indexOf("Mac") !== -1) os = 3;
if (navigator.userAgent.indexOf("X11") !== -1) os = 1;
if (navigator.userAgent.indexOf("Linux") !== -1) os = 1
return os
}
export const AddPeerPopup: React.FC<Props> = ({
greeting,
headline,
}) => {
const [openTab, setOpenTab] = useState(detectOSTab);
const [width, setWidth] = useState<number>(window.innerWidth);
const isMobile = width <= 768;
const items: TabsProps['items'] = [
{
key: "1",
label: <span><Icon component={LinuxSVG}/>Ubuntu</span>,
children: <UbuntuTab/>,
},
{
key: "2",
label: <span><WindowsFilled/>Windows</span>,
children: <WindowsTab/>,
},
{
key: "3",
label: <span><AppleFilled/>macOS</span>,
children: <MacTab/>,
},
/*{
key: "4",
label: <span><AndroidFilled/>Android</span>,
children: <></>,
},*/
{
key: "5",
label: <span><Icon component={DockerSVG}/>Docker</span>,
children: <DockerTab/>,
}
];
return <>
{greeting && <Paragraph
style={{textAlign: "center", whiteSpace: "pre-line", fontSize: "2em", marginBottom: -10}}>
{greeting}
</Paragraph>}
<Paragraph
style={{textAlign: "center", whiteSpace: "pre-line", fontSize: "2em"}}>
{headline}
</Paragraph>
<Paragraph type={"secondary"}
style={{
marginTop: "-15px",
textAlign: "center",
whiteSpace: "pre-line",
}}>
To get started install NetBird and log in using your {"\n"} email account.
</Paragraph>
<Tabs centered={!isMobile}
defaultActiveKey={openTab.toString()} tabPosition="top" animated={{inkBar: true, tabPane: false}}
items={items}/>
<Paragraph type={"secondary"}
style={{
marginTop: "15px",
}}>
After that you should be connected. Add more devices to your network or manage your existing devices in the
admin panel.
If you have further questions check out our {<Link target="_blank"
href={"https://netbird.io/docs/getting-started/installation"}>installation
guide</Link>}
</Paragraph>
</>
}
export default AddPeerPopup

View File

@@ -0,0 +1,63 @@
import React, {useState} from 'react';
import {StepCommand} from "./types"
import {formatDockerCommand, formatNetBirdUP} from "./common"
import SyntaxHighlighter from "react-syntax-highlighter";
import TabSteps from "./TabSteps";
import {Button, Typography} from "antd";
import Link from "antd/lib/typography/Link";
const {Title, Paragraph, Text} = Typography;
export const DockerTab = () => {
const [steps, setSteps] = useState([
{
key: 1,
title: 'Install Docker',
commands: (
<Button style={{marginTop: "5px"}} type="primary" href="https://docs.docker.com/engine/install/" target="_blank">Official Docker website</Button>
),
copied: false,
showCopyButton: false
} as StepCommand,
{
key: 2,
title: 'Run NetBird container',
commands: formatDockerCommand(),
copied: false,
showCopyButton: false
} as StepCommand,
{
key: 3,
title: "Read docs",
commands: (
<Link href="https://netbird.io/docs/getting-started/installation#running-netbird-in-docker" target="_blank">Running NetBird in Docker</Link>
),
copied: false,
showCopyButton: false
} as StepCommand
])
return (
<div style={{marginTop: 10}}>
{/*<Text style={{fontWeight: "bold"}}>
Run in Docker
</Text>
<div style={{fontSize: ".85em", marginTop: 5, marginBottom: 25}}>
<SyntaxHighlighter language="bash">
{formatDockerCommand()}
</SyntaxHighlighter>
</div>*/}
<Text style={{fontWeight: "bold"}}>
Install on Ubuntu
</Text>
<div style={{marginTop: 5}}>
<TabSteps stepsItems={steps}/>
</div>
</div>
)
}
export default DockerTab

View File

@@ -1,18 +1,49 @@
import React, {useState} from 'react';
import { Button } from "antd";
import {Button, Typography} from "antd";
import TabSteps from "./TabSteps";
import { StepCommand } from "./types"
import { formatNetBirdUP } from "./common"
import {StepCommand} from "./types"
import {formatNetBirdUP} from "./common"
import {Collapse} from "antd";
const { Panel } = Collapse;
const {Text} = Typography;
export const LinuxTab = () => {
const [quickSteps, setQuickSteps] = useState([
{
key: 1,
title: 'Download and run installer:',
commands: (
<Button style={{marginTop: "5px"}} type="primary" href="https://pkgs.netbird.io/windows/x64"
target="_blank">Download NetBird</Button>
),
copied: false
} as StepCommand,
{
key: 2,
title: 'Click on "Connect" from the NetBird icon in your system tray',
commands: '',
copied: false,
showCopyButton: false
},
{
key: 3,
title: 'Sign up using your email address',
commands: '',
copied: false,
showCopyButton: false
}
])
const [steps, setSteps] = useState([
{
key: 1,
title: 'Download and install Brew (package manager)',
title: 'Download and install Homebrew',
commands: (
<Button type="primary" href="https://brew.sh/" target="_blank">Download Brew</Button>
<Button style={{marginTop: "5px"}} type="primary" href="https://brew.sh/" target="_blank">Download
Brew</Button>
),
copied: false
} as StepCommand,
@@ -30,24 +61,49 @@ export const LinuxTab = () => {
} as StepCommand,
{
key: 3,
title: 'Run NetBird and log in the browser:',
commands: formatNetBirdUP(),
title: 'Start NetBird daemon:',
commands: [
`sudo netbird service install`,
`sudo netbird service start`
].join('\n'),
copied: false,
showCopyButton: true
} as StepCommand,
{
key: 4,
title: 'Get your IP address:',
commands: [
`sudo ifconfig utun100`
].join('\n'),
title: 'Run NetBird and log in the browser:',
commands: formatNetBirdUP(),
copied: false,
showCopyButton: true
} as StepCommand
])
return (
<TabSteps stepsItems={steps}/>
/*<div style={{marginTop: 10}}>
<Text style={{fontWeight: "bold"}}>
Install on macOS
</Text>
<div style={{marginTop: 5}}>
<TabSteps stepsItems={quickSteps}/>
</div>
<Collapse style={{marginLeft: -15}} bordered={false}>
<Panel header={<Text style={{fontWeight: "bold"}}>
Or install manually with Homebrew
</Text>} key="1">
<div style={{marginTop: 5}}>
<TabSteps stepsItems={steps}/>
</div>
</Panel>
</Collapse>
</div>*/
<div style={{marginTop: 10}}>
<Text style={{fontWeight: "bold"}}>
Install on macOS with Homebrew
</Text>
<div style={{marginTop: 5}}>
<TabSteps stepsItems={steps}/>
</div>
</div>
)
}

View File

@@ -2,16 +2,16 @@ import "highlight.js/styles/mono-blue.css";
import "highlight.js/lib/languages/bash";
import { StepCommand } from './types'
import SyntaxHighlighter from 'react-syntax-highlighter';
import { monoBlue } from 'react-syntax-highlighter/dist/esm/styles/hljs';
import {
Typography,
Space,
Steps, Button
Steps, Button, Popover, StepsProps
} from "antd";
import {copyToClipboard} from "../../utils/common";
import {CheckOutlined, CopyOutlined} from "@ant-design/icons";
import React, {useEffect, useState} from "react";
const { Step } = Steps;
const {Text} = Typography;
type Props = {
stepsItems: Array<StepCommand>
@@ -36,35 +36,22 @@ const TabSteps:React.FC<Props> = ({stepsItems}) => {
}, 2000)
}
}
return (
<Steps direction="vertical" current={0}>
<Steps direction="vertical" size={"small"}>
{steps.map(c =>
<Step
status={"process"}
key={c.key}
title={c.title}
title={<Text>{c.title}</Text>}
description={
<Space className="nb-code" direction="vertical" size="small" style={{display: "flex"}}>
<Space className="nb-code" direction="vertical" size="small" style={{display: "flex", fontSize: ".85em"}}>
{ (c.commands && (typeof c.commands === 'string')) ? (
<SyntaxHighlighter language="bash" style={monoBlue}>
<SyntaxHighlighter language="bash">
{c.commands}
</SyntaxHighlighter>
) : (
c.commands
)}
{ c.showCopyButton &&
<>
{ !c.copied ? (
<Button type="text" size="large" className="btn-copy-code" icon={<CopyOutlined/>}
style={{color: "rgb(107, 114, 128)"}}
onClick={() => onCopyClick(c.key, c.commands, true)}/>
): (
<Button type="text" size="large" className="btn-copy-code" icon={<CheckOutlined/>}
style={{color: "green"}}/>
)}
</>
}
</Space>
}
/>

View File

@@ -1,31 +1,29 @@
import React, { useState } from 'react';
import { Button } from "antd";
import React, {useState} from 'react';
import {StepCommand} from "./types"
import {formatNetBirdUP} from "./common"
import SyntaxHighlighter from "react-syntax-highlighter";
import TabSteps from "./TabSteps";
import { StepCommand } from "./types"
import { getConfig } from "../../config";
import Paragraph from 'antd/lib/skeleton/Paragraph';
import { formatNetBirdUP } from "./common"
import {Typography} from "antd";
const {Title, Paragraph, Text} = Typography;
export const UbuntuTab = () => {
const [steps, setSteps] = useState([
{
key: 1,
title: 'Add repository:',
title: 'Add repository',
commands: [
`sudo apt install ca-certificates curl gnupg -y`,
`curl -L https://pkgs.wiretrustee.com/debian/public.key | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/wiretrustee.gpg`,
`echo 'deb https://pkgs.wiretrustee.com/debian stable main' | sudo tee /etc/apt/sources.list.d/wiretrustee.list`
].join('\n'),
copied: false,
showCopyButton: true
showCopyButton: false
} as StepCommand,
{
key: 2,
title: 'Install NetBird:',
title: 'Install NetBird',
commands: [
`sudo apt-get update`,
`# for CLI only`,
@@ -34,39 +32,39 @@ export const UbuntuTab = () => {
`sudo apt-get install netbird-ui`
].join('\n'),
copied: false,
showCopyButton: true
showCopyButton: false
} as StepCommand,
{
key: 3,
title: 'Run NetBird and log in the browser:',
title: 'Run NetBird and log in the browser',
commands: formatNetBirdUP(),
copied: false,
showCopyButton: true
} as StepCommand,
{
key: 4,
title: 'Get your IP address:',
commands: [
`ip addr show wt0`
].join('\n'),
copied: false,
showCopyButton: true
} as StepCommand,
showCopyButton: false
} as StepCommand
])
/*const clickTest = () => {
steps.push({
key: steps.length+1,
title: `Test ${steps.length+1}`,
commands: [`hi lorena!`].join('\n'),
copied: false
})
console.log(steps)
setSteps([...steps])
}*/
return (
<TabSteps stepsItems={steps} />
<div style={{marginTop: 10}}>
{/*<Text style={{fontWeight: "bold"}}>
Install with one command
</Text>
<div style={{fontSize: ".85em", marginTop: 5, marginBottom: 25}}>
<SyntaxHighlighter language="bash">
curl -fsSL https://netbird.io/install.sh | sh
</SyntaxHighlighter>
</div>*/}
{/*<Text style={{fontWeight: "bold"}}>*/}
{/* Or install manually*/}
{/*</Text>*/}
<Text style={{fontWeight: "bold"}}>
Install on Ubuntu
</Text>
<div style={{marginTop: 5}}>
<TabSteps stepsItems={steps}/>
</div>
</div>
)
}

View File

@@ -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>
)
}

View File

@@ -3,11 +3,23 @@ const { grpcApiOrigin } = getConfig();
export const formatNetBirdUP = () => {
let cmd = "sudo netbird up"
let cmd = "netbird up"
if (grpcApiOrigin) {
cmd = "sudo netbird up --management-url " + 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')
}

View File

@@ -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

View 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

View 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

View File

@@ -7,7 +7,6 @@
"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"

View File

@@ -28,7 +28,6 @@ export function getConfig() {
scopesSupported: configJson.authScopesSupported,
apiOrigin: configJson.apiOrigin,
grpcApiOrigin: configJson.grpcApiOrigin,
latestVersion: configJson.latestVersion,
audience: configJson.authAudience,
hotjarTrackID: configJson.hotjarTrackID,
redirectURI: redirectURI,

View File

@@ -1,8 +1,4 @@
@import '~antd/dist/antd.css';
/*@tailwind base;*/
/*@tailwind components;*/
/*@tailwind utilities;*/
@import 'antd/dist/reset.css';
body {
font-size: 16px;
@@ -92,20 +88,6 @@ body {
align-self: center;
}
.ant-steps-item-tail::after {
background-color: #1890ff !important;
}
.ant-steps-item-icon {
background: #1890ff !important;
color: #ffffff !important;
border: none;
}
.ant-steps-icon {
color: #ffffff !important;
}
.nb-code {
margin-bottom: 1em;
}
@@ -135,4 +117,26 @@ body {
.access-control.ant-drawer-subtitle {
line-height: 22px;
margin: 24px 0;
}
.ant-steps-item-icon{
background: #EBEBEB !important;
border-color: #EBEBEB !important;
}
.ant-steps-icon-dot{
background: #EBEBEB !important;
border-color: #EBEBEB !important;
}
.ant-steps-icon {
background: #EBEBEB !important;
border: none;
color: black !important;
}
.ant-steps-item-tail::after {
background: #EBEBEB !important;
}
.ant-steps-item-tail {
border: none;
}

View File

@@ -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}`;

View File

@@ -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
}

View 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;

View 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 };

View 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
});

View 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),
]);
}

View 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
);
}
};

View 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
}

View File

@@ -11,6 +11,7 @@ import { sagas as routeSagas } from './route';
import { sagas as nameserverGroupSagas } from './nameservers';
import { sagas as eventSagas } from './event';
import { sagas as dnsSettingsSagas } from './dns-settings';
import { sagas as accountSagas } from './account';
import rootReducer from './root-reducer';
import { apiClient } from '../services/api-client';
@@ -31,5 +32,6 @@ sagaMiddleware.run(routeSagas);
sagaMiddleware.run(nameserverGroupSagas);
sagaMiddleware.run(eventSagas);
sagaMiddleware.run(dnsSettingsSagas);
sagaMiddleware.run(accountSagas);
export { apiClient, rootReducer, store };

View File

@@ -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 {

View File

@@ -7,6 +7,7 @@ import {actions as RouteActions} from './route';
import {actions as NameServerGroupActions} from './nameservers';
import {actions as EventActions} from './event';
import {actions as DNSSettingsActions} from './dns-settings';
import {actions as AccountActions} from './account';
export default {
peer: PeerActions,
@@ -17,5 +18,6 @@ export default {
route: RouteActions,
nameserverGroup: NameServerGroupActions,
event: EventActions,
dnsSettings: DNSSettingsActions
dnsSettings: DNSSettingsActions,
account: AccountActions
};

View File

@@ -9,6 +9,7 @@ import { reducer as route } from './route';
import { reducer as nameserverGroup } from './nameservers';
import { reducer as event } from './event';
import { reducer as dnsSettings } from './dns-settings';
import { reducer as account } from './account';
export default combineReducers({
peer,
@@ -19,5 +20,6 @@ export default combineReducers({
route,
nameserverGroup,
event,
dnsSettings
dnsSettings,
account
});

View File

@@ -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

View File

@@ -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;
};
@@ -24,6 +27,13 @@ export const capitalize = 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' });

24
src/utils/pageSize.tsx Normal file
View 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
}
}

View File

@@ -36,6 +36,7 @@ import {Group} from "../store/group/types";
import AccessControlModalGroups from "../components/AccessControlModalGroups";
import tableSpin from "../components/Spin";
import {useGetAccessTokenSilently} from "../utils/token";
import {usePageSizeHelpers} from "../utils/pageSize";
const {Title, Paragraph, Text} = Typography;
const {Column} = Table;
@@ -56,6 +57,7 @@ interface GroupsToShow {
}
export const AccessControl = () => {
const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers()
const {getAccessTokenSilently} = useGetAccessTokenSilently()
const dispatch = useDispatch()
@@ -68,7 +70,6 @@ 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);
@@ -76,11 +77,6 @@ export const AccessControl = () => {
const setupNewRuleVisible = useSelector((state: RootState) => state.rule.setupNewRuleVisible);
const [groupPopupVisible, setGroupPopupVisible] = useState(false as boolean | undefined)
const pageSizeOptions = [
{label: "5", value: "5"},
{label: "10", value: "10"},
{label: "15", value: "15"}
]
const optionsAllEnabled = [{label: 'Enabled', value: 'enabled'}, {label: 'All', value: 'all'}]
@@ -200,10 +196,6 @@ export const AccessControl = () => {
setOptionAllEnable(value)
}
const onChangePageSize = (value: string) => {
setPageSize(parseInt(value.toString()))
}
const showConfirmDelete = () => {
confirm({
icon: <ExclamationCircleOutlined/>,
@@ -327,7 +319,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 +328,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}
content={mainContent}
title={null}>
<Button type="link" onClick={() => setRuleAndView(rule)}>{label}</Button>
</Popover>
@@ -459,7 +450,7 @@ export const AccessControl = () => {
if (deletedRule.loading || savedRule.loading) return <></>
return <Dropdown.Button type="text" overlay={actionsMenu}
trigger={["click"]}
onVisibleChange={visible => {
onOpenChange={visible => {
if (visible) setRuleToAction(record as RuleDataTable)
}}></Dropdown.Button>
}}

View File

@@ -3,7 +3,7 @@ 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, Card, Col, Input, Row, Select, Space, Table, Typography,} from "antd";
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";
@@ -12,6 +12,9 @@ import UserUpdate from "../components/UserUpdate";
import {useOidcUser} from "@axa-fr/react-oidc";
import {capitalize, formatDateTime} from "../utils/common";
import {User} from "../store/user/types";
import {usePageSizeHelpers} from "../utils/pageSize";
import {QuestionCircleFilled} from "@ant-design/icons";
import {Link} from "react-router-dom";
const {Title, Paragraph, Text} = Typography;
const {Column} = Table;
@@ -20,6 +23,7 @@ interface EventDataTable extends Event {
}
export const Activity = () => {
const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers()
const {getAccessTokenSilently} = useGetAccessTokenSilently()
const {oidcUser} = useOidcUser();
const dispatch = useDispatch()
@@ -31,14 +35,8 @@ export const Activity = () => {
const setupKeys = useSelector((state: RootState) => state.setupKey.data);
const [textToSearch, setTextToSearch] = useState('');
const [pageSize, setPageSize] = useState(20);
const [dataTable, setDataTable] = useState([] as EventDataTable[]);
const pageSizeOptions = [
{label: "5", value: "5"},
{label: "10", value: "10"},
{label: "15", value: "15"},
{label: "20", value: "20"}
]
const transformDataTable = (d: Event[]): EventDataTable[] => {
return d.map(p => ({key: p.id, ...p} as EventDataTable))
@@ -73,10 +71,6 @@ export const Activity = () => {
setDataTable(transformDataTable(data))
}
const onChangePageSize = (value: string) => {
setPageSize(parseInt(value.toString()))
}
const getActivityRow = (group:string,text:string) => {
return <Row> <Text>Group <Text type="secondary">{group}</Text> {text}</Text> </Row>
}
@@ -150,6 +144,10 @@ export const Activity = () => {
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":
@@ -170,6 +168,8 @@ export const Activity = () => {
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":
@@ -177,10 +177,11 @@ export const Activity = () => {
return renderMultiRowSpan(event.meta.name, "Route for range " + event.meta.network_range)
case "user.group.add":
case "user.group.delete":
case "user.role.update":
if (user) {
return renderMultiRowSpan(user.name ? user.name : user.id,user.email ? user.email : "User")
}
return "n/a"
return "-"
case "setupkey.group.add":
case "setupkey.group.delete":
return renderMultiRowSpan(event.meta.setupkey,"Setup Key")
@@ -189,11 +190,17 @@ export const Activity = () => {
return renderMultiRowSpan(event.meta.peer_fqdn,event.meta.peer_ip)
case "dns.setting.disabled.management.group.add":
case "dns.setting.disabled.management.group.delete":
case "account.setting.peer.login.expiration.enable":
case "account.setting.peer.login.expiration.disable":
case "account.setting.peer.login.expiration.update":
return renderMultiRowSpan("","System setting")
case "user.invite":
if (user) {
return renderMultiRowSpan(user.name ? user.name : user.id,user.email ? user.email : "User")
}
break
default:
console.error("unknown event - missing handling", event.activity_code)
}
return event.target_id
@@ -216,8 +223,22 @@ export const Activity = () => {
<Space size="middle">
<Select value={pageSize.toString()} options={pageSizeOptions}
onChange={onChangePageSize} className="select-rows-per-page-en"/>
</Space>
</Col>
<Col xs={24}
sm={24}
md={5}
lg={5}
xl={5}
xxl={5} span={5}>
<Row justify="end">
<Col>
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
href="https://netbird.io/docs/how-to-guides/activity-monitoring">Learn more about activity tracking</Button>
</Col>
</Row>
</Col>
</Row>
{failed &&
<Alert message={failed.message} description={failed.data ? failed.data.message : " "}
@@ -269,4 +290,4 @@ export const Activity = () => {
)
}
export default Activity;
export default Activity;

View File

@@ -104,9 +104,6 @@ export const DNSSettingsForm = () => {
payload: dnsSettingsToSave
}))
})
.then(() => {
console.log("issued the request")
})
.catch((errorInfo) => {
let msg = "please check the fields and try again"
if (errorInfo.errorFields) {

View File

@@ -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>
);
};

View File

@@ -33,6 +33,7 @@ import {NameServer, NameServerGroup} from "../store/nameservers/types";
import NameServerGroupUpdate from "../components/NameServerGroupUpdate";
import {ExclamationCircleOutlined} from "@ant-design/icons";
import {useGetGroupTagHelpers} from "../utils/groups";
import {usePageSizeHelpers} from "../utils/pageSize";
const {Title, Paragraph} = Typography;
const {Column} = Table;
@@ -45,6 +46,7 @@ interface NameserverGroupDataTable extends NameServerGroup {
const styleNotification = {marginTop: 85}
export const Nameservers = () => {
const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers()
const {getAccessTokenSilently} = useGetAccessTokenSilently()
const dispatch = useDispatch()
@@ -63,14 +65,9 @@ export const Nameservers = () => {
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'}]
@@ -141,10 +138,6 @@ export const Nameservers = () => {
setDataTable(transformDataTable(filterDataTable()))
}
const onChangePageSize = (value: string) => {
setPageSize(parseInt(value.toString()))
}
const onClickEdit = () => {
dispatch(nsGroupActions.setSetupNewNameServerGroupVisible(true));
dispatch(nsGroupActions.setNameServerGroup({

View File

@@ -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 {ExclamationCircleOutlined, ReloadOutlined,} from '@ant-design/icons';
import {
Alert,
Button,
@@ -31,8 +32,6 @@ 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";
@@ -40,6 +39,9 @@ import {TooltipPlacement} from "antd/es/tooltip";
import {useGetAccessTokenSilently} from "../utils/token";
import {actions as userActions} from "../store/user";
import ButtonCopyMessage from "../components/ButtonCopyMessage";
import {usePageSizeHelpers} from "../utils/pageSize";
import AddPeerPopup from "../components/addpeer/AddPeerPopup";
import {getLocalItem, setLocalItem, StorageKey} from "../services/local";
const {Title, Paragraph, Text} = Typography;
const {Column} = Table;
@@ -47,7 +49,8 @@ const {confirm} = Modal;
export const Peers = () => {
//const {accessToken} = useOidcAccessToken()
const {onChangePageSize, pageSizeOptions, pageSize} = usePageSizeHelpers()
const {getAccessTokenSilently} = useGetAccessTokenSilently()
const dispatch = useDispatch()
@@ -62,20 +65,16 @@ 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 [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 [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 +91,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 +104,33 @@ export const Peers = () => {
})
}
useEffect(() => {
const refresh = () => {
dispatch(userActions.getUsers.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
dispatch(peerActions.getPeers.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
dispatch(groupActions.getGroups.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
dispatch(routeActions.getRoutes.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
}
useEffect(() => {
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 +195,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,11 +245,6 @@ export const Peers = () => {
setOptionOnOff(value)
}
const onChangePageSize = (value: string) => {
setPageSize(parseInt(value.toString()))
}
const showConfirmDelete = () => {
let peerRoutes: string[] = []
routes.forEach((r) => {
@@ -279,12 +290,11 @@ 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,
@@ -298,12 +308,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)
},
@@ -391,7 +400,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 +414,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 +425,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 +452,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 +470,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 +485,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 +496,7 @@ export const Peers = () => {
closable/>
}
<Card bodyStyle={{padding: 0}}>
<Table
{!showTutorial && (<Table
pagination={{
pageSize,
showSizeChanger: false,
@@ -512,9 +529,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"
@@ -553,8 +581,8 @@ export const Peers = () => {
render={(text, record, index) => {
let dt = new Date(text)
return <Popover content={dt.toLocaleString()}>
{(record as PeerDataTable).connected ? 'just now' : timeAgo(text)}
</Popover>
{(record as PeerDataTable).connected ? 'just now' : timeAgo(text)}
</Popover>
}}
/>
<Column title="OS" dataIndex="os"
@@ -572,17 +600,23 @@ export const Peers = () => {
}}></Dropdown.Button>
}}
/>
</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>
@@ -591,6 +625,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}
</>
)
}

View File

@@ -46,12 +46,14 @@ import {Group} from "../store/group/types";
import {TooltipPlacement} from "antd/es/tooltip";
import {actions as groupActions} from "../store/group";
import {useGetGroupTagHelpers} from "../utils/groups";
import {usePageSizeHelpers} from "../utils/pageSize";
const {Title, Paragraph, Text} = Typography;
const {Column} = Table;
const {confirm} = Modal;
export const Routes = () => {
const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers()
const {getAccessTokenSilently} = useGetAccessTokenSilently()
const dispatch = useDispatch()
const {
@@ -70,7 +72,6 @@ export const Routes = () => {
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);
@@ -79,13 +80,6 @@ export const Routes = () => {
const [groupPopupVisible, setGroupPopupVisible] = useState(false as boolean | undefined)
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 = [
@@ -211,10 +205,6 @@ export const Routes = () => {
setOptionAllEnable(value)
}
const onChangePageSize = (value: string) => {
setPageSize(parseInt(value.toString()))
}
const showConfirmDelete = () => {
confirm({
icon: <ExclamationCircleOutlined/>,

229
src/views/Settings.tsx Normal file
View File

@@ -0,0 +1,229 @@
import React, {useEffect, useState} from 'react';
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "typesafe-actions";
import {Button, Card, Col, Form, List, message, Modal, Radio, Row, Space, Typography,} from "antd";
import {useGetAccessTokenSilently} from "../utils/token";
import {useGetGroupTagHelpers} from "../utils/groups";
import {Container} from "../components/Container";
import UserUpdate from "../components/UserUpdate";
import ExpiresInInput, {expiresInToSeconds, secondsToExpiresIn} from "./ExpiresInInput";
import {checkExpiresIn} from "../utils/common";
import {actions as accountActions} from "../store/account";
import {Account, FormAccount} from "../store/account/types";
import {ExclamationCircleOutlined, QuestionCircleFilled} from "@ant-design/icons";
const {Title, Paragraph} = Typography;
const styleNotification = {marginTop: 85}
export const Settings = () => {
const {getAccessTokenSilently} = useGetAccessTokenSilently()
const dispatch = useDispatch()
const {
} = useGetGroupTagHelpers()
const accounts = useSelector((state: RootState) => state.account.data);
const failed = useSelector((state: RootState) => state.account.failed);
const loading = useSelector((state: RootState) => state.account.loading);
const updatedAccount = useSelector((state: RootState) => state.account.updatedAccount);
const users = useSelector((state: RootState) => state.user.data);
const [formAccount, setFormAccount] = useState({} as FormAccount);
const [accountToAction, setAccountToAction] = useState({} as FormAccount);
const [formPeerExpirationEnabled, setFormPeerExpirationEnabled] = useState(true);
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
const [form] = Form.useForm()
useEffect(() => {
dispatch(accountActions.getAccounts.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}));
}, [])
useEffect(() => {
if (accounts.length < 1) {
console.error("invalid account data returned from the Management API", accounts)
return
}
let account = accounts[0]
let fAccount = {
id: account.id,
settings: account.settings,
peer_login_expiration_formatted: secondsToExpiresIn(account.settings.peer_login_expiration, ["hour", "day"]),
peer_login_expiration_enabled: account.settings.peer_login_expiration_enabled
} as FormAccount
setFormAccount(fAccount)
setFormPeerExpirationEnabled(fAccount.peer_login_expiration_enabled)
form.setFieldsValue(fAccount)
}, [accounts])
const updatingSettings = 'updating_settings';
useEffect(() => {
if (updatedAccount.loading) {
message.loading({content: 'Saving...', key: updatingSettings, duration: 0, style: styleNotification});
} else if (updatedAccount.success) {
message.success({
content: 'Account settings have been successfully saved.',
key: updatingSettings,
duration: 2,
style: styleNotification
});
dispatch(accountActions.setUpdateAccount({...updatedAccount, success: false}));
dispatch(accountActions.resetUpdateAccount(null))
let fAccount = {
id: updatedAccount.data.id,
settings: updatedAccount.data.settings,
peer_login_expiration_formatted: secondsToExpiresIn(updatedAccount.data.settings.peer_login_expiration, ["hour", "day"]),
peer_login_expiration_enabled: updatedAccount.data.settings.peer_login_expiration_enabled
} as FormAccount
setFormAccount(fAccount)
} else if (updatedAccount.error) {
let errorMsg = "Failed to update account settings"
switch (updatedAccount.error.statusCode) {
case 403:
errorMsg = "Failed to update account settings. You might not have enough permissions."
break
default:
errorMsg = updatedAccount.error.data.message ? updatedAccount.error.data.message : errorMsg
break
}
message.error({
content: errorMsg,
key: updatingSettings,
duration: 5,
style: styleNotification
});
}
}, [updatedAccount])
const handleFormSubmit = () => {
form.validateFields()
.then((values) => {
confirmSave(values)
})
.catch((errorInfo) => {
let msg = "please check the fields and try again"
if (errorInfo.errorFields) {
msg = errorInfo.errorFields[0].errors[0]
}
message.error({
content: msg,
duration: 1,
});
});
}
const createAccountToSave = (values: FormAccount): Account => {
return {
id: formAccount.id,
settings: {
peer_login_expiration: expiresInToSeconds(values.peer_login_expiration_formatted),
peer_login_expiration_enabled: values.peer_login_expiration_enabled
}
} as Account
}
const confirmSave = (newValues: FormAccount) => {
if (newValues.peer_login_expiration_enabled != formAccount.peer_login_expiration_enabled) {
let content = newValues.peer_login_expiration_enabled ? "Enabling peer expiration will cause some peers added with the SSO login to disconnect, and re-authentication will be required. Do you want to enable peer login expiration?" : "Disabling peer expiration will cause peers added with the SSO login never to expire. For security reasons, keeping peers expiring periodically is usually better. Do you want to disable peer login expiration?"
confirmModal.confirm({
icon: <ExclamationCircleOutlined/>,
title: "Before you update your account settings.",
width: 600,
content: content,
onOk() {
saveAccount(newValues)
},
onCancel() {
},
});
} else {
saveAccount(newValues)
}
}
const saveAccount = (newValues: FormAccount) => {
let accountToSave = createAccountToSave(newValues)
dispatch(accountActions.updateAccount.request({
getAccessTokenSilently: getAccessTokenSilently,
payload: accountToSave
}))
}
return (
<>
<Container style={{paddingTop: "40px"}}>
<Row>
<Col span={24}>
<Title level={4}>Settings</Title>
<Paragraph>Manage your account's settings</Paragraph>
<Space direction="vertical" size="large" style={{display: 'flex'}}>
<Card bodyStyle={{padding: 0}}>
<Form
name="basic"
autoComplete="off"
form={form}
onFinish={handleFormSubmit}
>
<Space direction={"vertical"}
style={{display: 'flex'}}>
<Card
title="Authentication"
loading={loading}
defaultValue={"Enabled"}
>
<Form.Item
label="Peer login expiration"
name="peer_login_expiration_enabled"
tooltip="Peer login expiration allows to periodically request re-authentication of peers that were added with the SSO login. You can disable the expiration per peer in the peers tab."
//rules={[{validator: selectValidatorEmptyStrings}]}
>
<Radio.Group
options={[{label: 'Enabled', value: true}, {
label: 'Disabled',
value: false
}]}
optionType="button"
buttonStyle="solid"
onChange={function (e) {
setFormPeerExpirationEnabled(e.target.value)
}}
/>
</Form.Item>
<Form.Item name="peer_login_expiration_formatted"
label="Peer login expires in"
tooltip="Time after which every peer added with SSO login will require re-authentication."
rules={[{validator: checkExpiresIn}]}>
<ExpiresInInput
disabled={!formPeerExpirationEnabled}
options={Array.of({key: "hour", title: "Hours"}, {
key: "day",
title: "Days"
})
}/>
</Form.Item>
<Form.Item>
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
href="https://netbird.io/docs/how-to-guides/periodic-authentication">Learn more about login expiration</Button>
</Form.Item>
</Card>
<Form.Item style={{textAlign: 'center'}}>
<Button type="primary" htmlType="submit">
Save
</Button>
</Form.Item>
</Space>
</Form>
</Card>
</Space>
</Col>
</Row>
</Container>
<UserUpdate/>
{confirmModalContextHolder}
</>
)
}
export default Settings;

View File

@@ -34,6 +34,7 @@ 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 {usePageSizeHelpers} from "../utils/pageSize";
const {Title, Text, Paragraph} = Typography;
const {Column} = Table;
@@ -45,7 +46,7 @@ interface SetupKeyDataTable extends SetupKey {
}
export const SetupKeys = () => {
//const {accessToken} = useOidcAccessToken()
const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers()
const {getAccessTokenSilently} = useGetAccessTokenSilently()
const dispatch = useDispatch()
@@ -58,7 +59,6 @@ export const SetupKeys = () => {
const [textToSearch, setTextToSearch] = useState('');
const [optionValidAll, setOptionValidAll] = useState('valid');
const [pageSize, setPageSize] = useState(10);
const [dataTable, setDataTable] = useState([] as SetupKeyDataTable[]);
const [setupKeyToAction, setSetupKeyToAction] = useState(null as SetupKeyDataTable | null);
const setupNewKeyVisible = useSelector((state: RootState) => state.setupKey.setupNewKeyVisible)
@@ -66,11 +66,6 @@ export const SetupKeys = () => {
const styleNotification = {marginTop: 85}
const pageSizeOptions = [
{label: "5", value: "5"},
{label: "10", value: "10"},
{label: "15", value: "15"}
]
const optionsValidAll = [{label: 'Valid', value: 'valid'}, {label: 'All', value: 'all'}]
@@ -86,6 +81,7 @@ export const SetupKeys = () => {
]
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))
@@ -180,12 +176,8 @@ export const SetupKeys = () => {
setOptionValidAll(value)
}
const onChangePageSize = (value: string) => {
setPageSize(parseInt(value.toString()))
}
const showConfirmRevoke = () => {
confirm({
confirmModal.confirm({
icon: <ExclamationCircleOutlined/>,
width: 600,
content: <Space direction="vertical" size="small">
@@ -196,7 +188,6 @@ export const SetupKeys = () => {
</>
}
</Space>,
okType: 'danger',
onOk() {
dispatch(setupKeyActions.saveSetupKey.request({
getAccessTokenSilently: getAccessTokenSilently,
@@ -471,6 +462,7 @@ export const SetupKeys = () => {
</Container>
<SetupKeyNew/>
{confirmModalContextHolder}
</>
)
}

View File

@@ -1,4 +1,4 @@
import React, {useEffect, useRef, useState} from 'react';
import React, {useEffect, useState} from 'react';
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "typesafe-actions";
import {actions as userActions} from '../store/user';
@@ -28,11 +28,8 @@ 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 {useOidcIdToken, 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";
import {usePageSizeHelpers} from "../utils/pageSize";
const {Title, Paragraph, Text} = Typography;
const {Column} = Table;
@@ -44,9 +41,8 @@ interface UserDataTable extends User {
const styleNotification = {marginTop: 85}
export const Users = () => {
const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers()
const {getAccessTokenSilently} = useGetAccessTokenSilently()
const {oidcUser} = useOidcUser();
const {idTokenPayload} = useOidcIdToken()
const dispatch = useDispatch()
const groups = useSelector((state: RootState) => state.group.data)
@@ -59,14 +55,7 @@ export const Users = () => {
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"}
]
// setUserAndView makes the UserUpdate drawer visible (right side) and sets the user object
const setUserAndView = (user: User) => {
@@ -76,7 +65,8 @@ export const Users = () => {
email: user.email,
role: user.role,
auto_groups: user.auto_groups ? user.auto_groups : [],
name: user.name
name: user.name,
is_current: user.is_current
} as User));
}
@@ -96,22 +86,6 @@ export const Users = () => {
setDataTable(transformDataTable(filterDataTable()))
}, [textToSearch])
useEffect(() => {
let runUser = oidcUser
if (!oidcUser) {
runUser = idTokenPayload
}
if (runUser && runUser.sub) {
const found = users.find(u => u.id == runUser.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) =>
@@ -129,10 +103,6 @@ export const Users = () => {
setDataTable(transformDataTable(data))
}
const onChangePageSize = (value: string) => {
setPageSize(parseInt(value.toString()))
}
const onClickEdit = () => {
dispatch(userActions.setUpdateUserDrawerVisible(true));
dispatch(userActions.setUser({
@@ -319,17 +289,19 @@ export const Users = () => {
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>
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
if ((record as User).is_current) {
return <div>{btn}
<Tag color="blue">me</Tag>
</div>
}
return <div>{btn}
<Tag color="blue">me</Tag>
</div>
return btn
}}
/>
<Column title="Name" dataIndex="name"