mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac42a17b11 | ||
|
|
77ca3c6fde | ||
|
|
20e24b4ede | ||
|
|
7a29dac01c | ||
|
|
444e9ec44a | ||
|
|
dff0313f82 | ||
|
|
11fbfb336a | ||
|
|
4a0ae8f27d | ||
|
|
9a72d8b0c4 | ||
|
|
8e038cf242 | ||
|
|
5bd94eff56 | ||
|
|
77f065b093 | ||
|
|
a4d55cfb90 | ||
|
|
cfd4c9075b | ||
|
|
962180030a | ||
|
|
485e1e8d79 | ||
|
|
b11007b29f | ||
|
|
bce75c1ca9 | ||
|
|
0c09992b38 | ||
|
|
86b12f30d2 | ||
|
|
d3e34d8448 | ||
|
|
76083168f6 | ||
|
|
a54b3687ae | ||
|
|
25f154dc83 | ||
|
|
f3c7d877f8 | ||
|
|
7cea7e7f54 | ||
|
|
aaa351635f | ||
|
|
379ff5486e | ||
|
|
8bcd9918e2 | ||
|
|
044ccd0ce6 | ||
|
|
ab09ca3697 | ||
|
|
1644ed5dce | ||
|
|
cea459792f |
No files matched your search
@@ -52,14 +52,12 @@ export AUTH_SUPPORTED_SCOPES=${AUTH_SUPPORTED_SCOPES:-openid profile email api o
|
||||
export NETBIRD_MGMT_API_ENDPOINT=$(echo $NETBIRD_MGMT_API_ENDPOINT | sed -E 's/(:80|:443)$//')
|
||||
export NETBIRD_MGMT_GRPC_API_ENDPOINT=${NETBIRD_MGMT_GRPC_API_ENDPOINT}
|
||||
export NETBIRD_HOTJAR_TRACK_ID=${NETBIRD_HOTJAR_TRACK_ID}
|
||||
export NETBIRD_TOKEN_SOURCE=${NETBIRD_TOKEN_SOURCE:-accessToken}
|
||||
|
||||
REPO="https://github.com/netbirdio/netbird/"
|
||||
# this command will fetch the latest release e.g. v0.6.3
|
||||
export NETBIRD_LATEST_VERSION=$(basename $(curl -fs -o/dev/null -w %{redirect_url} ${REPO}releases/latest))
|
||||
echo "NetBird latest version: ${NETBIRD_LATEST_VERSION}"
|
||||
|
||||
# replace ENVs in the config
|
||||
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_LATEST_VERSION \$\$NETBIRD_HOTJAR_TRACK_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI"
|
||||
ENV_STR="\$\$USE_AUTH0 \$\$AUTH_AUDIENCE \$\$AUTH_AUTHORITY \$\$AUTH_CLIENT_ID \$\$AUTH_SUPPORTED_SCOPES \$\$NETBIRD_MGMT_API_ENDPOINT \$\$NETBIRD_MGMT_GRPC_API_ENDPOINT \$\$NETBIRD_HOTJAR_TRACK_ID \$\$AUTH_REDIRECT_URI \$\$AUTH_SILENT_REDIRECT_URI \$\$NETBIRD_TOKEN_SOURCE"
|
||||
|
||||
MAIN_JS=$(find /usr/share/nginx/html/static/js/main.*js)
|
||||
OIDC_TRUSTED_DOMAINS="/usr/share/nginx/html/OidcTrustedDomains.js"
|
||||
|
||||
Generated
+740
-437
File diff suppressed because it is too large.
Load diff
+4
-2
@@ -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",
|
||||
|
||||
+60
-47
@@ -4,7 +4,7 @@ import {apiClient, store} from "./store";
|
||||
import {hotjar} from 'react-hotjar';
|
||||
import {getConfig} from "./config";
|
||||
import Banner from "./components/Banner";
|
||||
import {Col, Layout, Row} from "antd";
|
||||
import {Col, ConfigProvider, Layout, Row} from "antd";
|
||||
import {Container} from "./components/Container";
|
||||
import Navbar from "./components/Navbar";
|
||||
import {Redirect, Route, Switch} from "react-router-dom";
|
||||
@@ -16,12 +16,12 @@ import SetupKeys from "./views/SetupKeys";
|
||||
import AccessControl from "./views/AccessControl";
|
||||
import Users from "./views/Users";
|
||||
import FooterComponent from "./components/FooterComponent";
|
||||
import {useGetAccessTokenSilently} from "./utils/token";
|
||||
import {useGetTokenSilently, useTokenSource} from "./utils/token";
|
||||
import {User} from "./store/user/types";
|
||||
import {SecureLoading} from "./components/Loading";
|
||||
import DNS from "./views/DNS";
|
||||
import Activity from "./views/Activity";
|
||||
|
||||
import Settings from "./views/Settings";
|
||||
|
||||
|
||||
const {Header, Content} = Layout;
|
||||
@@ -29,8 +29,9 @@ const {Header, Content} = Layout;
|
||||
function App() {
|
||||
const run = useRef(false)
|
||||
const [show, setShow] = useState(false)
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently();
|
||||
const {hotjarTrackID} = getConfig();
|
||||
const {hotjarTrackID,tokenSource} = getConfig();
|
||||
useTokenSource(tokenSource)
|
||||
const {getTokenSilently} = useGetTokenSilently();
|
||||
// @ts-ignore
|
||||
if (hotjarTrackID && window._DATADOG_SYNTHETICS_BROWSER === undefined) {
|
||||
hotjar.initialize(hotjarTrackID, 6);
|
||||
@@ -55,9 +56,9 @@ function App() {
|
||||
useEffect(() => {
|
||||
if (!run.current) {
|
||||
run.current = true
|
||||
apiClient.request<User[]>('GET', `/api/users`, {getAccessTokenSilently: getAccessTokenSilently})
|
||||
apiClient.request<User[]>('GET', `/api/users`, {getAccessTokenSilently: getTokenSilently})
|
||||
.then(() => {
|
||||
setShow(true)
|
||||
setShow(true)
|
||||
})
|
||||
.catch(e => {
|
||||
setShow(true)
|
||||
@@ -65,54 +66,66 @@ function App() {
|
||||
})
|
||||
}
|
||||
|
||||
}, [getAccessTokenSilently])
|
||||
}, [getTokenSilently])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
borderRadius: 4,
|
||||
colorPrimary: "#1890ff",
|
||||
fontFamily: "Arial"
|
||||
},
|
||||
components: {Badge: {fontSizeSM: 20}},
|
||||
}}
|
||||
>
|
||||
<Provider store={store}>
|
||||
{!show && <SecureLoading padding="3em" width={50} height={50}/>}
|
||||
{show &&
|
||||
<Layout>
|
||||
<Banner/>
|
||||
<Header className="header" style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-around",
|
||||
alignContent: "center"
|
||||
}}>
|
||||
<Row justify="space-around" align="middle">
|
||||
<Col span={24}>
|
||||
<Container>
|
||||
<Navbar/>
|
||||
</Container>
|
||||
</Col>
|
||||
</Row>
|
||||
</Header>
|
||||
<Content style={{minHeight: "100vh"}}>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
render={() => {
|
||||
return (
|
||||
<Redirect to="/peers"/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Route path='/peers' exact component={withOidcSecure(Peers)}/>
|
||||
<Route path="/add-peer" component={withOidcSecure(AddPeer)}/>
|
||||
<Route path="/setup-keys" component={withOidcSecure(SetupKeys)}/>
|
||||
<Route path="/acls" component={withOidcSecure(AccessControl)}/>
|
||||
<Route path="/routes" component={withOidcSecure(Routes)}/>
|
||||
<Route path="/users" component={withOidcSecure(Users)}/>
|
||||
<Route path="/dns" component={withOidcSecure(DNS)}/>
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ const AccessControlModalGroups:React.FC<Props> = ({data, title, visible, onCance
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal title={title} visible={isModalVisible} onCancel={() => onCancel()} footer={null}>
|
||||
<Modal title={title} open={isModalVisible} onCancel={() => onCancel()} footer={null}>
|
||||
<List
|
||||
itemLayout="horizontal"
|
||||
dataSource={data as Group[] | undefined}
|
||||
|
||||
@@ -23,7 +23,7 @@ import {Rule, RuleToSave} from "../store/rule/types";
|
||||
import {uniq} from "lodash"
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
const {Option} = Select;
|
||||
@@ -34,7 +34,7 @@ interface FormRule extends Rule {
|
||||
}
|
||||
|
||||
const AccessControlNew = () => {
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const setupNewRuleVisible = useSelector((state: RootState) => state.rule.setupNewRuleVisible)
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
@@ -103,7 +103,7 @@ const AccessControlNew = () => {
|
||||
.then((values) => {
|
||||
const ruleToSave = createRuleToSave()
|
||||
dispatch(ruleActions.saveRule.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: ruleToSave
|
||||
}))
|
||||
})
|
||||
@@ -408,9 +408,7 @@ const AccessControlNew = () => {
|
||||
<Col span={24}>
|
||||
<Divider></Divider>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
|
||||
href="https://docs.netbird.io/docs/overview/acls"
|
||||
style={{color: 'rgb(07, 114, 128)'}}>Learn
|
||||
more about access controls</Button>
|
||||
href="https://docs.netbird.io/docs/overview/acls">Learn more about access controls</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
@@ -36,7 +36,7 @@ const Banner = () => {
|
||||
|
||||
if((!stored_banner_closed || stored_banner_closed !== 'true') ||
|
||||
(!store_banner_md5 || store_banner_md5 !== announcement_md5)) {
|
||||
setShow(true);
|
||||
//setShow(true);
|
||||
localStorage.setItem(banner_md5_key,announcement_md5);
|
||||
localStorage.setItem(banner_closed_key,'false');
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ import {RuleObject} from "antd/lib/form";
|
||||
import cidrRegex from 'cidr-regex';
|
||||
import {NameServer, NameServerGroup, NameServerGroupToSave} from "../store/nameservers/types";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups"
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
|
||||
@@ -51,7 +51,7 @@ const NameServerGroupUpdate = () => {
|
||||
selectValidator
|
||||
} = useGetGroupTagHelpers()
|
||||
const dispatch = useDispatch()
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const {Option} = Select;
|
||||
const nsGroup = useSelector((state: RootState) => state.nameserverGroup.nameserverGroup)
|
||||
const setupNewNameServerGroupVisible = useSelector((state: RootState) => state.nameserverGroup.setupNewNameServerGroupVisible)
|
||||
@@ -220,7 +220,7 @@ const NameServerGroupUpdate = () => {
|
||||
.then((values) => {
|
||||
const nsGroupToSave = createNSGroupToSave(values as NameServerGroup)
|
||||
dispatch(nsGroupActions.saveNameServerGroup.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: nsGroupToSave
|
||||
}))
|
||||
|
||||
@@ -604,9 +604,7 @@ const NameServerGroupUpdate = () => {
|
||||
<Col span={24}>
|
||||
<Divider></Divider>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
|
||||
href="https://netbird.io/docs/how-to-guides/nameservers"
|
||||
style={{color: 'rgb(07, 114, 128)'}}>Learn
|
||||
more about nameservers</Button>
|
||||
href="https://netbird.io/docs/how-to-guides/nameservers">Learn more about nameservers</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>) :
|
||||
|
||||
@@ -11,7 +11,8 @@ import {User} from "../store/user/types";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as userActions} from "../store/user";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {actions as personalAccessTokenActions} from "../store/personal-access-token";
|
||||
|
||||
const {useBreakpoint} = Grid;
|
||||
|
||||
@@ -19,7 +20,7 @@ const Navbar = () => {
|
||||
let location = useLocation();
|
||||
const config = getConfig();
|
||||
const { logout } = useOidc();
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const {oidcUser} = useOidcUser();
|
||||
@@ -35,23 +36,37 @@ const Navbar = () => {
|
||||
|
||||
const items = [
|
||||
{label: (<Link to="/peers">Peers</Link>), key: '/peers'},
|
||||
{label: (<Link to="/add-peer">Add Peer</Link>), key: '/add-peer'},
|
||||
{label: (<Link to="/setup-keys">Setup Keys</Link>), key: '/setup-keys'},
|
||||
{label: (<Link to="/acls">Access Control</Link>), key: '/acls'},
|
||||
{label: (<Link to="/routes">Network Routes</Link>), key: '/routes'},
|
||||
{ label: (<Link to="/dns">DNS</Link>), key: '/dns' },
|
||||
{label: (<Link to="/users">Users</Link>), key: '/users'},
|
||||
{label: (<Link to="/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});
|
||||
|
||||
const openPersonalUserPage = () => {
|
||||
dispatch(userActions.setUser({
|
||||
id: currentUser.id,
|
||||
email: currentUser.email,
|
||||
role: currentUser.role,
|
||||
auto_groups: currentUser.auto_groups ? currentUser.auto_groups : [],
|
||||
name: currentUser.name,
|
||||
is_current: currentUser.is_current,
|
||||
is_service_user: currentUser.is_service_user,
|
||||
} as User));
|
||||
dispatch(userActions.setUserTabOpen("Users"));
|
||||
dispatch(userActions.setEditUserPopupVisible(true));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const fs = items.filter(m => showTab(m?.key?.toString(), currentUser) && m?.key !== userEmailKey && m?.key !== userLogoutKey && m?.key !== userDividerKey)
|
||||
if (screens.xs === true) {
|
||||
@@ -80,7 +95,7 @@ const Navbar = () => {
|
||||
window.location.pathname !== '/peers' &&
|
||||
window.location.pathname !== '/users') {
|
||||
setIsRefreshingUserState(true)
|
||||
dispatch(userActions.getUsers.request({getAccessTokenSilently: getAccessTokenSilently, payload: null}))
|
||||
dispatch(userActions.getUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}))
|
||||
return
|
||||
}
|
||||
if (users.length === 0 && isRefreshingUserState) {
|
||||
@@ -114,7 +129,7 @@ const Navbar = () => {
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
label: <>{user?.email}</>,
|
||||
label: (<Link to="/users" onClick={openPersonalUserPage}>{user?.email}</Link>),
|
||||
key: '0',
|
||||
},
|
||||
{
|
||||
@@ -148,6 +163,10 @@ const Navbar = () => {
|
||||
<Col flex="1 1 auto">
|
||||
<div>
|
||||
<Menu mode="horizontal" selectable={true} selectedKeys={[location.pathname]}
|
||||
onSelect={(e) => {
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null))
|
||||
}}
|
||||
defaultSelectedKeys={[location.pathname]} items={menuItems}/>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
@@ -2,14 +2,14 @@ import React, {useEffect, useRef, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as peerActions} from '../store/peer';
|
||||
import {Button, Col, Collapse, Divider, Drawer, Form, Input, Row, Select, Space, Tag, Typography} from "antd";
|
||||
import {Button, Col, Collapse, Divider, Drawer, Form, Input, Radio, Row, Select, Space, Tag, Typography} from "antd";
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import type {CustomTagProps} from 'rc-select/lib/BaseSelect'
|
||||
import {FormPeer, Peer, PeerGroupsToSave} from "../store/peer/types";
|
||||
import {Group, GroupPeer} from "../store/group/types";
|
||||
import {CloseOutlined, EditOutlined} from "@ant-design/icons";
|
||||
import {RuleObject} from 'antd/lib/form';
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {timeAgo} from "../utils/common";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
@@ -18,7 +18,8 @@ const {Panel} = Collapse;
|
||||
const punycode = require('punycode/')
|
||||
|
||||
const PeerUpdate = () => {
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const users = useSelector((state: RootState) => state.user.data)
|
||||
@@ -190,6 +191,7 @@ const PeerUpdate = () => {
|
||||
setCallingPeerAPI(false)
|
||||
setCallingPeerAPI(false)
|
||||
setSubmitRunning(false)
|
||||
setEstimatedName("")
|
||||
}
|
||||
|
||||
const noUpdateToGroups = (): Boolean => {
|
||||
@@ -200,6 +202,10 @@ const PeerUpdate = () => {
|
||||
return !formPeer.name || formPeer.name === peer.name
|
||||
}
|
||||
|
||||
const noUpdateToLoginExpiration = (): Boolean => {
|
||||
return formPeer.login_expiration_enabled === peer.login_expiration_enabled
|
||||
}
|
||||
|
||||
const onChange = (data: any) => {
|
||||
setFormPeer({...formPeer, ...data})
|
||||
}
|
||||
@@ -232,7 +238,8 @@ const PeerUpdate = () => {
|
||||
return {
|
||||
id: formPeer.id,
|
||||
ssh_enabled: formPeer.ssh_enabled,
|
||||
name: formPeer.name
|
||||
name: formPeer.name,
|
||||
login_expiration_enabled: formPeer.login_expiration_enabled
|
||||
} as Peer
|
||||
}
|
||||
|
||||
@@ -240,18 +247,18 @@ const PeerUpdate = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
setSubmitRunning(true)
|
||||
if (!noUpdateToName()) {
|
||||
if (!noUpdateToName() || !noUpdateToLoginExpiration()) {
|
||||
const peerUpdate = createPeerToSave()
|
||||
setCallingPeerAPI(true)
|
||||
dispatch(peerActions.updatePeer.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: peerUpdate
|
||||
}))
|
||||
}
|
||||
if (peerGroupsToSave.groupsToRemove.length || peerGroupsToSave.groupsToAdd.length || peerGroupsToSave.groupsNoId.length) {
|
||||
if (!noUpdateToGroups()) {
|
||||
setCallingGroupAPI(true)
|
||||
dispatch(peerActions.saveGroups.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: peerGroupsToSave
|
||||
}))
|
||||
}
|
||||
@@ -303,7 +310,7 @@ const PeerUpdate = () => {
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button onClick={onCancel} disabled={savedGroups.loading}>Cancel</Button>
|
||||
<Button type="primary"
|
||||
disabled={(savedGroups.loading || updatedPeers.loading) || (noUpdateToGroups() && noUpdateToName())}
|
||||
disabled={(savedGroups.loading || updatedPeers.loading) || (noUpdateToGroups() && noUpdateToName() && noUpdateToLoginExpiration())}
|
||||
onClick={handleFormSubmit}>Save</Button>
|
||||
</Space>
|
||||
}
|
||||
@@ -435,6 +442,20 @@ const PeerUpdate = () => {
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)}
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="login_expiration_enabled"
|
||||
label="Login expiration"
|
||||
tooltip="When login expires, the user has to re-authenticate this peer. This only applies to peers added with the SSO login."
|
||||
>
|
||||
<Radio.Group
|
||||
options={[{label: 'Enabled', value: true}, {label: 'Disabled', value: false}]}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
disabled={!formPeer.user_id}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="groupsNames"
|
||||
@@ -458,23 +479,6 @@ const PeerUpdate = () => {
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{/*<Col span={24}>
|
||||
<Row wrap={false} gutter={12}>
|
||||
<Col flex="none">
|
||||
<FlagFilled/>
|
||||
</Col>
|
||||
<Col flex="auto">
|
||||
<Paragraph>
|
||||
Every peer is part of the group All, thus you can't remove it.
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>*/}
|
||||
{/*<Col span={24}>
|
||||
<Divider orientation="left" plain style={{color: "#5a5c5a"}}>
|
||||
System Info
|
||||
</Divider>
|
||||
</Col>*/}
|
||||
<Col span={24}>
|
||||
<Collapse onChange={onChange} bordered={false} ghost={true}
|
||||
style={{color: "#5a5c5a"}}>
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
routePeerSeparator,
|
||||
transformGroupedDataTable
|
||||
} from '../utils/routes'
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import {useGetGroupTagHelpers} from "../utils/groups";
|
||||
|
||||
const {Paragraph} = Typography;
|
||||
@@ -50,7 +50,7 @@ const RouteUpdate = () => {
|
||||
selectValidator
|
||||
} = useGetGroupTagHelpers()
|
||||
const {Option} = Select;
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const setupNewRouteVisible = useSelector((state: RootState) => state.route.setupNewRouteVisible)
|
||||
const setupNewRouteHA = useSelector((state: RootState) => state.route.setupNewRouteHA)
|
||||
@@ -123,7 +123,8 @@ const RouteUpdate = () => {
|
||||
peers.forEach((p) => {
|
||||
let os: string
|
||||
os = p.os
|
||||
if (!os.toLowerCase().startsWith("darwin") && !os.toLowerCase().startsWith("windows")) {
|
||||
if (!os.toLowerCase().startsWith("darwin") && !os.toLowerCase().startsWith("windows") && !os.toLowerCase().startsWith("android")
|
||||
&& route && !routes.filter(r => r.network_id === route.network_id).find(r => r.peer === p.id)) {
|
||||
options?.push({
|
||||
label: peerToPeerIP(p.name, p.ip),
|
||||
value: peerToPeerIP(p.name, p.ip),
|
||||
@@ -135,10 +136,14 @@ const RouteUpdate = () => {
|
||||
const createRouteToSave = (inputRoute: FormRoute): RouteToSave => {
|
||||
let peerIDList = inputRoute.peer.split(routePeerSeparator)
|
||||
let peerID: string
|
||||
if (peerIDList[1]) {
|
||||
peerID = peerIPToID[peerIDList[1]]
|
||||
if (peerIDList.length === 1) {
|
||||
peerID = inputRoute.peer
|
||||
} else {
|
||||
peerID = peerIPToID[peerNameToIP[inputRoute.peer]]
|
||||
if (peerIDList[1]) {
|
||||
peerID = peerIPToID[peerIDList[1]]
|
||||
} else {
|
||||
peerID = peerIPToID[peerNameToIP[inputRoute.peer]]
|
||||
}
|
||||
}
|
||||
|
||||
let [ existingGroups, groupsToCreate ] = getExistingAndToCreateGroupsLists(inputRoute.groups)
|
||||
@@ -163,7 +168,7 @@ const RouteUpdate = () => {
|
||||
if (!setupNewRouteHA || formRoute.peer != '') {
|
||||
const routeToSave = createRouteToSave(formRoute)
|
||||
dispatch(routeActions.saveRoute.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: routeToSave
|
||||
}))
|
||||
} else {
|
||||
@@ -180,7 +185,7 @@ const RouteUpdate = () => {
|
||||
}
|
||||
const routeToSave = createRouteToSave(updateRoute)
|
||||
dispatch(routeActions.saveRoute.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: routeToSave
|
||||
}))
|
||||
})
|
||||
@@ -453,9 +458,7 @@ const RouteUpdate = () => {
|
||||
<Col span={24}>
|
||||
<Divider></Divider>
|
||||
<Button icon={<QuestionCircleFilled/>} type="link" target="_blank"
|
||||
href="https://netbird.io/docs/how-to-guides/network-routes"
|
||||
style={{color: 'rgb(07, 114, 128)'}}>Learn
|
||||
more about network routes</Button>
|
||||
href="https://netbird.io/docs/how-to-guides/network-routes">Learn more about network routes</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
@@ -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 {useGetTokenSilently} from "../utils/token";
|
||||
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())
|
||||
@@ -52,7 +54,7 @@ const customLastUsedFormat: DatePickerProps['format'] = value => {
|
||||
}
|
||||
|
||||
const SetupKeyNew = () => {
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const setupNewKeyVisible = useSelector((state: RootState) => state.setupKey.setupNewKeyVisible)
|
||||
const setupKey = useSelector((state: RootState) => state.setupKey.setupKey)
|
||||
@@ -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,38 +121,13 @@ 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()
|
||||
.then((values) => {
|
||||
let setupKeyToSave = createSetupKeyToSave()
|
||||
dispatch(setupKeyActions.saveSetupKey.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: setupKeyToSave
|
||||
}))
|
||||
})
|
||||
@@ -282,7 +261,7 @@ const SetupKeyNew = () => {
|
||||
}
|
||||
|
||||
const groupsChanged = (): boolean => {
|
||||
if (formSetupKey.autoGroupNames.length != setupKey.auto_groups.length) {
|
||||
if (setupKey && setupKey.auto_groups && formSetupKey.autoGroupNames.length != setupKey.auto_groups.length) {
|
||||
return true
|
||||
}
|
||||
const formGroupIds = groups?.filter(g => formSetupKey.autoGroupNames.includes(g.name)).map(g => g.id || '') || []
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,481 @@
|
||||
import {
|
||||
Badge,
|
||||
Breadcrumb,
|
||||
Button,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
List, Modal,
|
||||
Row,
|
||||
Select,
|
||||
Skeleton,
|
||||
Space, Table,
|
||||
Tag, Typography
|
||||
} from "antd";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {actions as userActions} from "../store/user";
|
||||
import {FormUser, User, UserToSave} from "../store/user/types";
|
||||
import {useGetTokenSilently} from "../utils/token";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import {CustomTagProps} from "rc-select/lib/BaseSelect";
|
||||
import {actions as groupActions} from "../store/group";
|
||||
import {actions as personalAccessTokenActions} from "../store/personal-access-token";
|
||||
import {PersonalAccessToken, PersonalAccessTokenCreate, SpecificPAT} from "../store/personal-access-token/types";
|
||||
import tableSpin from "./Spin";
|
||||
import AddPATPopup from "./popups/AddPATPopup";
|
||||
import {fullDate} from "../utils/common";
|
||||
import {ExclamationCircleOutlined} from "@ant-design/icons";
|
||||
import {Container} from "./Container";
|
||||
import Column from "antd/lib/table/Column";
|
||||
import {useOidcUser} from "@axa-fr/react-oidc";
|
||||
|
||||
const {Option} = Select;
|
||||
const {Title, Paragraph, Text} = Typography;
|
||||
|
||||
interface TokenDataTable extends PersonalAccessToken {
|
||||
key: string
|
||||
status: string
|
||||
created_by_email: string
|
||||
}
|
||||
|
||||
const UserEdit = () => {
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const users = useSelector((state: RootState) => state.user.data)
|
||||
const user = useSelector((state: RootState) => state.user.user)
|
||||
const savedUser = useSelector((state: RootState) => state.user.savedUser)
|
||||
const personalAccessTokens = useSelector((state: RootState) => state.personalAccessToken.data);
|
||||
const tab = useSelector((state: RootState) => state.user.userTabOpen)
|
||||
|
||||
const loading = useSelector((state: RootState) => state.user.loading);
|
||||
|
||||
const {oidcUser} = useOidcUser();
|
||||
const [tokenTable, setTokenTable] = useState([] as TokenDataTable[]);
|
||||
|
||||
const [tagGroups, setTagGroups] = useState([] as string[])
|
||||
const [currentGroups, setCurrentGroups] = useState([] as string[])
|
||||
|
||||
const [formUser, setFormUser] = useState({} as FormUser)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedUser.loading) return
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null))
|
||||
setFormUser({} as FormUser)
|
||||
dispatch(userActions.setEditUserPopupVisible(false));
|
||||
}
|
||||
|
||||
const createUserToSave = (values: any): UserToSave => {
|
||||
const autoGroups = groups?.filter(g => values.autoGroupsNames.includes(g.name)).map(g => g.id || '') || []
|
||||
// find groups that do not yet exist (newly added by the user)
|
||||
const allGroupsNames: string[] = groups?.map(g => g.name);
|
||||
const groupsToCreate = values.autoGroupsNames.filter((s: string) => !allGroupsNames.includes(s))
|
||||
let userID = user ? user.id : ''
|
||||
let isServiceUser = user ? user.is_service_user : false
|
||||
return {
|
||||
id: userID,
|
||||
role: values.role,
|
||||
name: values.name,
|
||||
groupsToCreate: groupsToCreate,
|
||||
auto_groups: autoGroups,
|
||||
is_service_user: isServiceUser
|
||||
} as UserToSave
|
||||
}
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
let userToSave = createUserToSave(values)
|
||||
dispatch(userActions.saveUser.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: userToSave
|
||||
}))
|
||||
dispatch(userActions.setEditUserPopupVisible(false));
|
||||
dispatch(userActions.setUser(null as unknown as User))
|
||||
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null))
|
||||
dispatch(groupActions.getGroups.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null
|
||||
}));
|
||||
dispatch(userActions.getServiceUsers.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null
|
||||
}))
|
||||
dispatch(userActions.getUsers.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null
|
||||
}))
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log('errorInfo', errorInfo)
|
||||
});
|
||||
};
|
||||
|
||||
const onClickAddNewPersonalAccessToken = () => {
|
||||
dispatch(personalAccessTokenActions.setPersonalAccessToken({
|
||||
user_id: "",
|
||||
name: "",
|
||||
expires_in: 7
|
||||
} as PersonalAccessTokenCreate))
|
||||
dispatch(personalAccessTokenActions.setNewPersonalAccessTokenPopupVisible(true));
|
||||
}
|
||||
|
||||
const onBreadcrumbUsersClick = (key: string) => {
|
||||
if (savedUser.loading) return
|
||||
dispatch(userActions.setUser(null as unknown as User));
|
||||
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null))
|
||||
dispatch(userActions.setUserTabOpen(key))
|
||||
}
|
||||
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = []
|
||||
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v)
|
||||
}
|
||||
})
|
||||
|
||||
if (hasSpaceNamed.length) {
|
||||
return Promise.reject(new Error("Group names with just spaces are not allowed"))
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const tagRender = (props: CustomTagProps) => {
|
||||
const {label, value, closable, onClose} = props;
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<Tag
|
||||
color="blue"
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{value}</strong>
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{margin: '8px 0'}}/>
|
||||
<Row style={{padding: '0 8px 4px'}}>
|
||||
<Col flex="auto">
|
||||
<span style={{color: "#9CA3AF"}}>Add new group by pressing "Enter"</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
|
||||
fill="#9CA3AF"/>
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
|
||||
const optionRender = (label: string) => {
|
||||
let peersCount = ''
|
||||
const g = groups.find(_g => _g.name === label)
|
||||
if (g) peersCount = ` - ${g.peers_count || 0} ${(!g.peers_count || parseInt(g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{label}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const transformTokenTable = (d: PersonalAccessToken[]): TokenDataTable[] => {
|
||||
if(!d) {
|
||||
return []
|
||||
}
|
||||
return d.map(p => ({
|
||||
key: p.id,
|
||||
status: Date.parse(p.expiration_date) > Date.now() ? "valid" : "expired",
|
||||
created_by_email: getEmail(p),
|
||||
...p} as TokenDataTable))
|
||||
}
|
||||
|
||||
const getEmail = (token: PersonalAccessToken): string => {
|
||||
return users.find(u => u.id === token.created_by)?.email || ""
|
||||
}
|
||||
|
||||
const showConfirmDelete = (token: TokenDataTable) => {
|
||||
confirmModal.confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
title: "Delete token \"" + token.name + "\"",
|
||||
width: 600,
|
||||
content: <Space direction="vertical" size="small">
|
||||
<Paragraph>Are you sure you want to delete this token?</Paragraph>
|
||||
</Space>,
|
||||
onOk() {
|
||||
dispatch(personalAccessTokenActions.deletePersonalAccessToken.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: {
|
||||
user_id: user.id,
|
||||
id: token.id,
|
||||
name: token.name,
|
||||
} as SpecificPAT
|
||||
}));
|
||||
},
|
||||
onCancel() {
|
||||
// noop
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setTokenTable(transformTokenTable(personalAccessTokens))
|
||||
}, [personalAccessTokens, users])
|
||||
|
||||
useEffect(() => {
|
||||
setTagGroups(groups?.filter(g => g.name != "All").map(g => g.name) || [])
|
||||
}, [groups])
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
// @ts-ignore
|
||||
setCurrentGroups(groups.filter(g => g.name != "All" && user.auto_groups.includes(g.id)).map(g => g.name) || [])
|
||||
}
|
||||
}, [groups, user])
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(userActions.getUsers.request({getAccessTokenSilently: getTokenSilently, payload: null}))
|
||||
dispatch(groupActions.getGroups.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: null
|
||||
}))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (user.is_current || user.is_service_user) {
|
||||
dispatch(personalAccessTokenActions.getPersonalAccessTokens.request({getAccessTokenSilently: getTokenSilently,
|
||||
payload: user.id}))
|
||||
}
|
||||
}, [user])
|
||||
|
||||
useEffect(() => {
|
||||
if (user && currentGroups) {
|
||||
form.setFieldsValue({
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
email: user.email,
|
||||
autoGroupsNames: currentGroups,
|
||||
})
|
||||
}
|
||||
}, [form, user, currentGroups])
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<Text onClick={() => onBreadcrumbUsersClick("Users")}>
|
||||
Users
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: (
|
||||
<Text onClick={() => onBreadcrumbUsersClick("Service Users")}>
|
||||
Service Users
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const isUserAdmin = (userId: string): boolean => {
|
||||
return users.find(u => u.id === userId)?.role === "admin"
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{paddingTop: "13px"}}>
|
||||
<Breadcrumb style={{marginBottom: "30px"}}
|
||||
items={[
|
||||
{
|
||||
title: <Text onClick={() => onBreadcrumbUsersClick("Users")}>All Users</Text>,
|
||||
},
|
||||
{
|
||||
title: <Text onClick={() => onBreadcrumbUsersClick(tab)}>{tab}</Text>,
|
||||
// menu: { items: menuItems },
|
||||
},
|
||||
{
|
||||
title: user.name,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Container style={{backgroundColor: "white", padding: "20px", borderRadius: "4px", boxSizing: "border-box", border: "0.5px solid #D9D9D9", marginBottom: "7px"}}>
|
||||
<div style={{maxWidth: "800px"}}>
|
||||
<Paragraph style={{textAlign: "left", whiteSpace: "pre-line", fontSize: "1.5em"}}>{user.name}</Paragraph>
|
||||
<Form layout="vertical" hideRequiredMark form={form}
|
||||
initialValues={{
|
||||
name: formUser.name,
|
||||
role: formUser.role,
|
||||
email: formUser.email,
|
||||
autoGroupsNames: formUser.autoGroupsNames,
|
||||
}}
|
||||
>
|
||||
<Row style={{ paddingBottom: "15px"}}>
|
||||
{!user.is_service_user && <Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
|
||||
<Form.Item
|
||||
name="email"
|
||||
label={<Text style={{fontSize: "16px", fontWeight: "500"}}>Email</Text>}
|
||||
style={{marginRight: "70px"}}
|
||||
>
|
||||
<Input
|
||||
disabled={user.id}
|
||||
value={formUser.email}
|
||||
style={{color: "#5a5c5a"}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>}
|
||||
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
|
||||
<Form.Item
|
||||
name="role"
|
||||
label={<Text style={{fontSize: "16px", fontWeight: "500"}}>Role</Text>}
|
||||
style={{marginRight: "50px"}}
|
||||
>
|
||||
<Select style={{width: '100%'}}
|
||||
disabled={user.is_current}>
|
||||
<Option value="admin">admin</Option>
|
||||
<Option value="user">user</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
{!user.is_service_user && <Row style={{ paddingBottom: "15px"}}>
|
||||
<Col span={9}>
|
||||
<Form.Item
|
||||
name="autoGroupsNames"
|
||||
label={<Text style={{fontSize: "16px", fontWeight: "500"}}>Auto-assigned groups</Text>}
|
||||
tooltip="Every peer enrolled with this user will be automatically added to these groups"
|
||||
rules={[{validator: selectValidator}]}
|
||||
>
|
||||
<Select mode="tags"
|
||||
style={{width: '100%'}}
|
||||
placeholder="Associate groups with the user"
|
||||
tagRender={tagRender}
|
||||
dropdownRender={dropDownRender}
|
||||
disabled={oidcUser && !isUserAdmin(oidcUser.sub)}
|
||||
>
|
||||
{
|
||||
tagGroups.map(m =>
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>}
|
||||
<Space style={{display: 'flex', justifyContent: 'start'}}>
|
||||
<Button disabled={loading} onClick={onCancel}>Cancel</Button>
|
||||
<Button type="primary"
|
||||
onClick={handleFormSubmit}>Save</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</div>
|
||||
</Container>
|
||||
{user && (user.is_current || user.is_service_user) && <Container style={{backgroundColor: "white", padding: "20px", borderRadius: "4px", boxSizing: "border-box", border: "0.5px solid #D9D9D9"}}>
|
||||
<div style={{maxWidth: "800px"}}>
|
||||
<Paragraph style={{textAlign: "left", whiteSpace: "pre-line", fontSize: "1.5em"}}>Access tokens</Paragraph>
|
||||
<Row gutter={21} style={{marginTop: "-22px", marginBottom: "10px"}}>
|
||||
<Col xs={24} sm={24} md={20} lg={20} xl={20} xxl={20} span={20}>
|
||||
<Paragraph type={"secondary"} style={{textAlign: "left", whiteSpace: "pre-line"}}>Access tokens give access to the Netbird API</Paragraph>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={1} lg={1} xl={1} xxl={1} span={1} style={{marginTop: "-16px"}}>
|
||||
{personalAccessTokens && personalAccessTokens.length > 0 && <Button type="primary" onClick={onClickAddNewPersonalAccessToken}>Create Token</Button>}
|
||||
</Col>
|
||||
</Row>
|
||||
{personalAccessTokens && personalAccessTokens.length > 0 &&
|
||||
<Table
|
||||
size={"small"}
|
||||
style={{marginTop: "-10px"}}
|
||||
showHeader={false}
|
||||
scroll={{x: 800}}
|
||||
pagination={false}
|
||||
loading={tableSpin(loading)}
|
||||
dataSource={tokenTable}>
|
||||
<Column className={"non-highlighted-table-column"} sorter={(a, b) => ((a as TokenDataTable).created_at.localeCompare((b as TokenDataTable).created_at))}
|
||||
defaultSortOrder='descend'
|
||||
render={(text, record, index) => {
|
||||
return (<>
|
||||
<Row>
|
||||
<Col>
|
||||
<Badge status={(record as TokenDataTable).status === "valid" ? "success" : "error"} style={{marginTop: "1px", marginRight: "5px", marginLeft: "0px"}}/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Paragraph style={{fontSize: "16px", fontWeight: "500", margin: "0px", padding: "0px"}}>{(record as TokenDataTable).name}</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{fontSize: "13px", fontWeight: "400", margin: "0px", marginTop: "-2px", padding: "0px"}}>{"Created" + ((record as TokenDataTable).created_by_email && user.is_service_user ? " by " + (record as TokenDataTable).created_by_email : "") + " on " + fullDate((record as TokenDataTable).created_at)}</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
</>)
|
||||
}}/>
|
||||
<Column render={(text, record, index) => {
|
||||
return <>
|
||||
<Paragraph type={"secondary"} style={{textAlign: "left", fontSize: "11px"}}>Expires on</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{textAlign: "left", marginTop: "-10px", marginBottom: "0", fontSize: "15px"}}>{fullDate((record as TokenDataTable).expiration_date)}</Paragraph>
|
||||
</>
|
||||
}}
|
||||
/>
|
||||
<Column render={(text, record, index) => {
|
||||
return <>
|
||||
<Paragraph type={"secondary"} style={{textAlign: "left", fontSize: "11px"}}>Last used</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{textAlign: "left", marginTop: "-10px", marginBottom: "0", fontSize: "15px"}}>{(record as TokenDataTable).last_used ? fullDate((record as TokenDataTable).last_used) : "Never"}</Paragraph>
|
||||
</>
|
||||
}}
|
||||
/>
|
||||
<Column align="right"
|
||||
render={(text, record, index) => {
|
||||
return (
|
||||
<Button danger={true} type={"text"}
|
||||
onClick={() => {
|
||||
showConfirmDelete(record as TokenDataTable)
|
||||
}}
|
||||
>Delete</Button>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Table>}
|
||||
<Divider style={{marginTop: "-12px"}}></Divider>
|
||||
{(personalAccessTokens === null || personalAccessTokens.length === 0) && <Space direction="vertical" size="small" align="start"
|
||||
style={{display: 'flex', padding: '45px 0px', marginTop: "-40px", justifyContent: 'center'}}>
|
||||
<Paragraph
|
||||
style={{textAlign: "start", whiteSpace: "pre-line", fontSize: "16px"}}>
|
||||
You don’t have any access tokens yet.
|
||||
</Paragraph>
|
||||
<Button type="primary" onClick={onClickAddNewPersonalAccessToken}>Create Token</Button>
|
||||
</Space>}
|
||||
</div>
|
||||
</Container>}
|
||||
</div>
|
||||
<AddPATPopup/>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default UserEdit;
|
||||
@@ -1,414 +0,0 @@
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {Alert, Button, Col, Divider, Drawer, Form, Input, Modal, Row, Select, Space, Tag, Typography} from "antd";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {CloseOutlined, EditOutlined, ExclamationCircleOutlined} from "@ant-design/icons";
|
||||
import {Header} from "antd/es/layout/layout";
|
||||
import {Group} from "../store/group/types";
|
||||
import {FormUser, User, UserToSave} from "../store/user/types";
|
||||
import {RuleObject} from "antd/lib/form";
|
||||
import {CustomTagProps} from "rc-select/lib/BaseSelect";
|
||||
import {actions as userActions} from "../store/user";
|
||||
import {useGetAccessTokenSilently} from "../utils/token";
|
||||
import {useOidcUser} from "@axa-fr/react-oidc";
|
||||
|
||||
const {Paragraph, Text} = Typography;
|
||||
|
||||
const {confirm} = Modal;
|
||||
|
||||
const {Option} = Select;
|
||||
|
||||
const UserUpdate = () => {
|
||||
const {oidcUser} = useOidcUser();
|
||||
const {getAccessTokenSilently} = useGetAccessTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
const user = useSelector((state: RootState) => state.user.user)
|
||||
const savedUser = useSelector((state: RootState) => state.user.savedUser)
|
||||
const groups = useSelector((state: RootState) => state.group.data)
|
||||
const users = useSelector((state: RootState) => state.user.data)
|
||||
const updateUserDrawerVisible = useSelector((state: RootState) => state.user.updateUserDrawerVisible)
|
||||
const [selectedTagGroups, setSelectedTagGroups] = useState([] as string[])
|
||||
const [tagGroups, setTagGroups] = useState([] as string[])
|
||||
const [editName, setEditName] = useState(false)
|
||||
const inputNameRef = useRef<any>(null)
|
||||
|
||||
const [formUser, setFormUser] = useState({} as FormUser)
|
||||
const [currentUser, setCurrentUser] = useState({} as User)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
useEffect(() => {
|
||||
if (editName) inputNameRef.current!.focus({
|
||||
cursor: 'end',
|
||||
});
|
||||
}, [editName]);
|
||||
|
||||
const toggleEditName = (status: boolean) => {
|
||||
setEditName(status);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setTagGroups(groups?.filter(g => g.name != "All").map(g => g.name) || [])
|
||||
}, [groups])
|
||||
useEffect(() => {
|
||||
if (oidcUser && oidcUser.sub) {
|
||||
const found = users.find(u => u.id == oidcUser.sub)
|
||||
if (found) {
|
||||
setCurrentUser(found)
|
||||
}
|
||||
} else {
|
||||
setCurrentUser({} as User)
|
||||
}
|
||||
|
||||
}, [oidcUser, users])
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return
|
||||
|
||||
let allGroups = new Map<string, Group>();
|
||||
groups.forEach(g => {
|
||||
allGroups.set(g.id!, g)
|
||||
})
|
||||
|
||||
if (!user.auto_groups) {
|
||||
user.auto_groups = []
|
||||
}
|
||||
let formKeyGroups = user.auto_groups.filter(g => allGroups.get(g)).map(g => allGroups.get(g)!.name)
|
||||
|
||||
const fUser = {
|
||||
...user,
|
||||
autoGroupsNames: user.auto_groups ? formKeyGroups : [],
|
||||
} as FormUser
|
||||
setFormUser(fUser)
|
||||
form.setFieldsValue(fUser)
|
||||
}, [user])
|
||||
|
||||
const selectValidator = (_: RuleObject, value: string[]) => {
|
||||
let hasSpaceNamed = []
|
||||
|
||||
value.forEach(function (v: string) {
|
||||
if (!v.trim().length) {
|
||||
hasSpaceNamed.push(v)
|
||||
}
|
||||
})
|
||||
|
||||
if (hasSpaceNamed.length) {
|
||||
return Promise.reject(new Error("Group names with just spaces are not allowed"))
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const tagRender = (props: CustomTagProps) => {
|
||||
const {label, value, closable, onClose} = props;
|
||||
const onPreventMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<Tag
|
||||
color="blue"
|
||||
onMouseDown={onPreventMouseDown}
|
||||
closable={closable}
|
||||
onClose={onClose}
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{value}</strong>
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
const optionRender = (label: string) => {
|
||||
let peersCount = ''
|
||||
const g = groups.find(_g => _g.name === label)
|
||||
if (g) peersCount = ` - ${g.peers_count || 0} ${(!g.peers_count || parseInt(g.peers_count) !== 1) ? 'peers' : 'peer'} `
|
||||
return (
|
||||
<>
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{marginRight: 3}}
|
||||
>
|
||||
<strong>{label}</strong>
|
||||
</Tag>
|
||||
<span style={{fontSize: ".85em"}}>{peersCount}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const dropDownRender = (menu: React.ReactElement) => (
|
||||
<>
|
||||
{menu}
|
||||
<Divider style={{margin: '8px 0'}}/>
|
||||
<Row style={{padding: '0 8px 4px'}}>
|
||||
<Col flex="auto">
|
||||
<span style={{color: "#9CA3AF"}}>Add new group by pressing "Enter"</span>
|
||||
</Col>
|
||||
<Col flex="none">
|
||||
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.70455 7.19176V5.89915H10.3949C10.7727 5.89915 11.1174 5.80634 11.429 5.62074C11.7405 5.43513 11.9875 5.18655 12.1697 4.875C12.3554 4.56345 12.4482 4.21875 12.4482 3.84091C12.4482 3.46307 12.3554 3.12003 12.1697 2.81179C11.9841 2.50024 11.7356 2.25166 11.424 2.06605C11.1158 1.88044 10.7727 1.78764 10.3949 1.78764H9.83807V0.5H10.3949C11.0114 0.5 11.5715 0.650805 12.0753 0.952414C12.5791 1.25402 12.9818 1.65672 13.2834 2.16051C13.585 2.6643 13.7358 3.22443 13.7358 3.84091C13.7358 4.30161 13.648 4.73414 13.4723 5.13849C13.3 5.54285 13.0613 5.89915 12.7564 6.20739C12.4515 6.51562 12.0968 6.75758 11.6925 6.93324C11.2881 7.10559 10.8556 7.19176 10.3949 7.19176H1.70455ZM4.90128 11.0646L0.382102 6.54545L4.90128 2.02628L5.79119 2.91619L2.15696 6.54545L5.79119 10.1747L4.90128 11.0646Z"
|
||||
fill="#9CA3AF"/>
|
||||
</svg>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
|
||||
const handleChangeTags = (value: string[]) => {
|
||||
let validatedValues: string[] = []
|
||||
value.forEach(function (v) {
|
||||
if (v.trim().length) {
|
||||
validatedValues.push(v)
|
||||
}
|
||||
})
|
||||
setSelectedTagGroups(validatedValues)
|
||||
};
|
||||
|
||||
const createUserToSave = (): UserToSave => {
|
||||
const autoGroups = groups?.filter(g => formUser.autoGroupsNames.includes(g.name)).map(g => g.id || '') || []
|
||||
// find groups that do not yet exist (newly added by the user)
|
||||
const allGroupsNames: string[] = groups?.map(g => g.name);
|
||||
const groupsToCreate = formUser.autoGroupsNames.filter(s => !allGroupsNames.includes(s))
|
||||
return {
|
||||
id: formUser.id,
|
||||
email: formUser.email,
|
||||
role: formUser.role,
|
||||
name: formUser.name,
|
||||
groupsToCreate: groupsToCreate,
|
||||
auto_groups: autoGroups,
|
||||
} as UserToSave
|
||||
}
|
||||
|
||||
const showConfirmChangeRole = (userToSave: UserToSave) => {
|
||||
let content = <Paragraph>With this action, you will remove the administrative privileges of your user.
|
||||
Your user will be limited to read-only operations in this account. Are you sure?</Paragraph>
|
||||
let contentModule = <div>{content}</div>
|
||||
|
||||
let name = formUser ? formUser.email : ''
|
||||
confirm({
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
title: "Update user \"" + name + "\"",
|
||||
width: 600,
|
||||
content: contentModule,
|
||||
okType: 'danger',
|
||||
onOk() {
|
||||
dispatch(userActions.saveUser.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: userToSave
|
||||
}))
|
||||
},
|
||||
onCancel() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// check if currentUser (who is doing the modification) removes the administrative privileges from themselves
|
||||
const isShowConfirmWarning = (userToSave: UserToSave): boolean => {
|
||||
return currentUser.id == userToSave.id && currentUser.role === "admin" && userToSave.role !== "admin"
|
||||
}
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
let userToSave = createUserToSave()
|
||||
if (isShowConfirmWarning(userToSave)) {
|
||||
showConfirmChangeRole(userToSave)
|
||||
} else {
|
||||
dispatch(userActions.saveUser.request({
|
||||
getAccessTokenSilently: getAccessTokenSilently,
|
||||
payload: userToSave
|
||||
}))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log('errorInfo', errorInfo)
|
||||
});
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
if (savedUser.loading) return
|
||||
dispatch(userActions.setUser({
|
||||
id: "",
|
||||
email: "",
|
||||
role: "",
|
||||
status: "",
|
||||
auto_groups: [],
|
||||
name: user.name,
|
||||
is_current: user.is_current,
|
||||
} as User));
|
||||
setFormUser({} as FormUser)
|
||||
toggleEditName(false)
|
||||
dispatch(userActions.setUpdateUserDrawerVisible(false));
|
||||
}
|
||||
|
||||
const onChange = (data: any) => {
|
||||
setFormUser({...formUser, ...data})
|
||||
}
|
||||
|
||||
const changesDetected = (): boolean => {
|
||||
return emailChanged() || nameChanged() || groupsChanged() || roleChanged()
|
||||
}
|
||||
|
||||
const emailChanged = (): boolean => {
|
||||
return formUser.email !== user.email
|
||||
}
|
||||
|
||||
const roleChanged = (): boolean => {
|
||||
return formUser.role !== user.role
|
||||
}
|
||||
|
||||
const nameChanged = (): boolean => {
|
||||
return formUser.name !== user.name
|
||||
}
|
||||
|
||||
const groupsChanged = (): boolean => {
|
||||
if (!formUser.autoGroupsNames) {
|
||||
return false
|
||||
}
|
||||
if (formUser.autoGroupsNames.length != user.auto_groups.length) {
|
||||
return true
|
||||
}
|
||||
const formGroupIds = groups?.filter(g => formUser.autoGroupsNames.includes(g.name)).map(g => g.id || '') || []
|
||||
|
||||
return user.auto_groups?.filter(g => !formGroupIds.includes(g)).length > 0
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{user &&
|
||||
<Drawer
|
||||
forceRender={true}
|
||||
headerStyle={{display: "none"}}
|
||||
open={updateUserDrawerVisible}
|
||||
bodyStyle={{paddingBottom: 80}}
|
||||
onClose={onCancel}
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
<Button disabled={savedUser.loading} onClick={onCancel}>Cancel</Button>
|
||||
<Button type="primary" disabled={savedUser.loading || !changesDetected()}
|
||||
onClick={handleFormSubmit}>{`${formUser.id ? 'Save' : 'Invite'}`}</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form layout="vertical" hideRequiredMark form={form} onValuesChange={onChange}
|
||||
initialValues={{
|
||||
["role"]: formUser.role
|
||||
}}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Header style={{margin: "-32px -24px 20px -24px", padding: "24px 24px 0 24px"}}>
|
||||
<Row align="top">
|
||||
{/*Close Icon*/}
|
||||
<Col flex="none" style={{display: "flex"}}>
|
||||
{!editName && user.id &&
|
||||
<button type="button" aria-label="Close" className="ant-drawer-close"
|
||||
style={{paddingTop: 3}}
|
||||
onClick={onCancel}>
|
||||
<span role="img" aria-label="close"
|
||||
className="anticon anticon-close">
|
||||
<CloseOutlined size={16}/>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</Col>
|
||||
{/* Name Label*/}
|
||||
<Col flex="auto">
|
||||
{!editName && user.id && formUser.name !== "" ? (
|
||||
<div className={"access-control input-text ant-drawer-title"}
|
||||
onClick={() => toggleEditName(true)}>{formUser.name ? formUser.name : formUser.name}
|
||||
<EditOutlined/></div>
|
||||
) : (
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Name"
|
||||
rules={[{
|
||||
required: false,
|
||||
message: 'Please add a new name for this user',
|
||||
whitespace: true
|
||||
}]}
|
||||
>
|
||||
<Input
|
||||
placeholder={formUser.name}
|
||||
ref={inputNameRef}
|
||||
onPressEnter={() => toggleEditName(false)}
|
||||
onBlur={() => toggleEditName(false)}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Header>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="email"
|
||||
label="Email"
|
||||
>
|
||||
<Input
|
||||
disabled={user.id}
|
||||
value={formUser.email}
|
||||
style={{color: "#5a5c5a"}}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="role"
|
||||
label="Role"
|
||||
>
|
||||
<Select
|
||||
style={{width: '100%'}}
|
||||
disabled={currentUser.role != null && currentUser.role !== "admin"}>
|
||||
<Option value="admin">admin</Option>
|
||||
<Option value="user">user</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item
|
||||
name="autoGroupsNames"
|
||||
label="Auto-assigned groups"
|
||||
tooltip="Every peer enrolled with this user will be automatically added to these groups"
|
||||
rules={[{validator: selectValidator}]}
|
||||
>
|
||||
<Select mode="tags"
|
||||
style={{width: '100%'}}
|
||||
placeholder="Associate groups with the user"
|
||||
tagRender={tagRender}
|
||||
onChange={handleChangeTags}
|
||||
disabled={currentUser.role != null && currentUser.role !== "admin"}
|
||||
dropdownRender={dropDownRender}
|
||||
>
|
||||
{
|
||||
tagGroups.map(m =>
|
||||
<Option key={m}>{optionRender(m)}</Option>
|
||||
)
|
||||
}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Divider></Divider>
|
||||
</Col>
|
||||
{currentUser && currentUser.role !== "admin" && (
|
||||
<div>
|
||||
<Col span={24}>
|
||||
<Alert
|
||||
message={<div style={{color: "#5a5c5a"}}>
|
||||
You are not an administrator, therefore you can't update users.</div>}
|
||||
showIcon={false}
|
||||
type="warning"/>
|
||||
</Col>
|
||||
<br></br>
|
||||
</div>
|
||||
)}
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
</Drawer>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserUpdate
|
||||
@@ -1,54 +0,0 @@
|
||||
import React, {useState} from 'react';
|
||||
|
||||
import { Button } from "antd";
|
||||
import TabSteps from "./TabSteps";
|
||||
import { StepCommand } from "./types"
|
||||
import { formatNetBirdUP } from "./common"
|
||||
|
||||
export const LinuxTab = () => {
|
||||
|
||||
const [steps, setSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: 'Download and install Brew (package manager)',
|
||||
commands: (
|
||||
<Button type="primary" href="https://brew.sh/" target="_blank">Download Brew</Button>
|
||||
),
|
||||
copied: false
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 2,
|
||||
title: 'Install NetBird:',
|
||||
commands: [
|
||||
`# for CLI only`,
|
||||
`brew install netbirdio/tap/netbird`,
|
||||
`# for GUI package`,
|
||||
`brew install --cask netbirdio/tap/netbird-ui`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 3,
|
||||
title: 'Run NetBird and log in the browser:',
|
||||
commands: formatNetBirdUP(),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 4,
|
||||
title: 'Get your IP address:',
|
||||
commands: [
|
||||
`sudo ifconfig utun100`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand
|
||||
])
|
||||
|
||||
return (
|
||||
<TabSteps stepsItems={steps}/>
|
||||
)
|
||||
}
|
||||
|
||||
export default LinuxTab
|
||||
@@ -1,73 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Button } from "antd";
|
||||
import TabSteps from "./TabSteps";
|
||||
import { StepCommand } from "./types"
|
||||
import { getConfig } from "../../config";
|
||||
import Paragraph from 'antd/lib/skeleton/Paragraph';
|
||||
import { formatNetBirdUP } from "./common"
|
||||
|
||||
|
||||
|
||||
export const UbuntuTab = () => {
|
||||
|
||||
const [steps, setSteps] = useState([
|
||||
{
|
||||
key: 1,
|
||||
title: 'Add repository:',
|
||||
commands: [
|
||||
`sudo apt install ca-certificates curl gnupg -y`,
|
||||
`curl -L https://pkgs.wiretrustee.com/debian/public.key | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/wiretrustee.gpg`,
|
||||
`echo 'deb https://pkgs.wiretrustee.com/debian stable main' | sudo tee /etc/apt/sources.list.d/wiretrustee.list`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 2,
|
||||
title: 'Install NetBird:',
|
||||
commands: [
|
||||
`sudo apt-get update`,
|
||||
`# for CLI only`,
|
||||
`sudo apt-get install netbird`,
|
||||
`# for GUI package`,
|
||||
`sudo apt-get install netbird-ui`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 3,
|
||||
title: 'Run NetBird and log in the browser:',
|
||||
commands: formatNetBirdUP(),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
{
|
||||
key: 4,
|
||||
title: 'Get your IP address:',
|
||||
commands: [
|
||||
`ip addr show wt0`
|
||||
].join('\n'),
|
||||
copied: false,
|
||||
showCopyButton: true
|
||||
} as StepCommand,
|
||||
])
|
||||
|
||||
/*const clickTest = () => {
|
||||
steps.push({
|
||||
key: steps.length+1,
|
||||
title: `Test ${steps.length+1}`,
|
||||
commands: [`hi lorena!`].join('\n'),
|
||||
copied: false
|
||||
})
|
||||
console.log(steps)
|
||||
setSteps([...steps])
|
||||
}*/
|
||||
|
||||
return (
|
||||
<TabSteps stepsItems={steps} />
|
||||
)
|
||||
}
|
||||
|
||||
export default UbuntuTab
|
||||
@@ -1,13 +0,0 @@
|
||||
import {getConfig} from "../../config";
|
||||
const { grpcApiOrigin } = getConfig();
|
||||
|
||||
|
||||
export const formatNetBirdUP = () => {
|
||||
let cmd = "sudo netbird up"
|
||||
if (grpcApiOrigin) {
|
||||
cmd = "sudo netbird up --management-url " + grpcApiOrigin
|
||||
}
|
||||
return [
|
||||
cmd
|
||||
].join('\n')
|
||||
}
|
||||
@@ -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 |
@@ -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 |
@@ -0,0 +1,222 @@
|
||||
import {useGetTokenSilently} from "../../utils/token";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {RootState} from "typesafe-actions";
|
||||
import {Button, Col, Divider, Form, Input, InputNumber, message, Modal, Row, Space, Typography} from "antd";
|
||||
import {Container} from "../Container";
|
||||
import {CheckOutlined, CopyOutlined, QuestionCircleFilled} from "@ant-design/icons";
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {actions as personalAccessTokenActions} from "../../store/personal-access-token";
|
||||
import {PersonalAccessTokenCreate} from "../../store/personal-access-token/types";
|
||||
import {copyToClipboard} from "../../utils/common";
|
||||
|
||||
const {Title, Text, Paragraph} = Typography;
|
||||
|
||||
const ExpiresInDefault = 30
|
||||
const styleNotification = {marginTop: 85}
|
||||
|
||||
const AddPATPopup = () => {
|
||||
const {getTokenSilently} = useGetTokenSilently()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const user = useSelector((state: RootState) => state.user.user)
|
||||
|
||||
const addTokenModalOpen = useSelector((state: RootState) => state.personalAccessToken.newPersonalAccessTokenPopupVisible)
|
||||
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
|
||||
const [showPlainToken, setShowPlainToken] = useState(false);
|
||||
const [tokenCopied, setTokenCopied] = useState(false);
|
||||
const [plainToken, setPlainToken] = useState("")
|
||||
const inputNameRef = useRef<any>(null)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const savedPersonalAccessToken = useSelector((state: RootState) => state.personalAccessToken.savedPersonalAccessToken);
|
||||
|
||||
const onCopyClick = (text: string, copied: boolean) => {
|
||||
copyToClipboard(text)
|
||||
setTokenCopied(true)
|
||||
if (copied) {
|
||||
setTimeout(() => {
|
||||
onCopyClick(text, false)
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setShowPlainToken(false)
|
||||
setTokenCopied(false)
|
||||
if (savedPersonalAccessToken.loading) return
|
||||
dispatch(personalAccessTokenActions.setPersonalAccessToken({
|
||||
user_id: "",
|
||||
name: "",
|
||||
expires_in: 7
|
||||
} as PersonalAccessTokenCreate))
|
||||
form.resetFields()
|
||||
dispatch(personalAccessTokenActions.setNewPersonalAccessTokenPopupVisible(false));
|
||||
dispatch(personalAccessTokenActions.setSavedPersonalAccessToken({...savedPersonalAccessToken, success: false}));
|
||||
dispatch(personalAccessTokenActions.resetSavedPersonalAccessToken(null))
|
||||
}
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
form.validateFields()
|
||||
.then((values) => {
|
||||
let personalAccessTokenToSave = {
|
||||
user_id: user.id,
|
||||
name: values.name,
|
||||
expires_in: values.expires_in,
|
||||
} as PersonalAccessTokenCreate
|
||||
dispatch(personalAccessTokenActions.savePersonalAccessToken.request({
|
||||
getAccessTokenSilently: getTokenSilently,
|
||||
payload: personalAccessTokenToSave
|
||||
}))
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
console.log('errorInfo', errorInfo)
|
||||
});
|
||||
};
|
||||
|
||||
const createKey = 'saving';
|
||||
useEffect(() => {
|
||||
if (savedPersonalAccessToken.loading) {
|
||||
message.loading({content: 'Saving...', key: createKey, duration: 0, style: styleNotification});
|
||||
} else if (savedPersonalAccessToken.success) {
|
||||
message.destroy(createKey)
|
||||
setPlainToken(savedPersonalAccessToken.data.plain_token)
|
||||
setShowPlainToken(true)
|
||||
form.resetFields()
|
||||
} else if (savedPersonalAccessToken.error) {
|
||||
message.error({
|
||||
content: 'Failed to create personal access token. You might not have enough permissions.',
|
||||
key: createKey,
|
||||
duration: 2,
|
||||
style: styleNotification
|
||||
});
|
||||
dispatch(personalAccessTokenActions.setNewPersonalAccessTokenPopupVisible(false));
|
||||
setShowPlainToken(false)
|
||||
setTokenCopied(false)
|
||||
dispatch(personalAccessTokenActions.setSavedPersonalAccessToken({...savedPersonalAccessToken, error: null}));
|
||||
dispatch(personalAccessTokenActions.resetSavedPersonalAccessToken(null))
|
||||
}
|
||||
}, [savedPersonalAccessToken])
|
||||
|
||||
useEffect(() => {
|
||||
const keyDownHandler = (event: any) => {
|
||||
console.log('User pressed: ', event.key);
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
handleFormSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', keyDownHandler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', keyDownHandler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
open={addTokenModalOpen}
|
||||
onCancel={onCancel}
|
||||
footer={
|
||||
<Space style={{display: 'flex', justifyContent: 'end'}}>
|
||||
{!showPlainToken && <Button disabled={savedPersonalAccessToken.loading} onClick={onCancel}>{"Cancel"}</Button>}
|
||||
{!showPlainToken && <Button type="primary" disabled={showPlainToken}
|
||||
onClick={handleFormSubmit}>{"Create token"}</Button>}
|
||||
{showPlainToken && <Button type="primary" disabled={!showPlainToken} onClick={onCancel}>Done</Button>}
|
||||
</Space>
|
||||
}
|
||||
width={460}
|
||||
>
|
||||
<Container style={{textAlign: "start"}}>
|
||||
<Paragraph
|
||||
style={{textAlign: "start", whiteSpace: "pre-line", fontSize: "22px", fontWeight: "500"}}>
|
||||
{showPlainToken ? "Token created successfully!" : "Create a Personal Access Token"}
|
||||
</Paragraph>
|
||||
{!showPlainToken && <Paragraph type={"secondary"}
|
||||
style={{
|
||||
textAlign: "start",
|
||||
fontSize: "14px",
|
||||
whiteSpace: "pre-line",
|
||||
marginTop: "-23px",
|
||||
paddingBottom: "25px",
|
||||
}}>
|
||||
{"This token can be used to authenticate against" + "\n" + "NetBird's Public API."}
|
||||
</Paragraph>}
|
||||
{showPlainToken && <Paragraph type={"secondary"} style={{
|
||||
textAlign: "start",
|
||||
fontSize: "14px",
|
||||
whiteSpace: "pre-line",
|
||||
marginTop: "40px",
|
||||
}}>{"This token will not be shown again, so be sure to copy it and" + "\n" + "store in a secure location."}</Paragraph>}
|
||||
{!showPlainToken && <Form layout="vertical" hideRequiredMark form={form}
|
||||
initialValues={{
|
||||
expires_in: ExpiresInDefault,
|
||||
}}
|
||||
>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Row align="top">
|
||||
<Col flex="auto">
|
||||
<Paragraph style={{fontSize: "16px", fontWeight: "500", marginTop: "-8px"}}>Token name</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-18px"}}>Create a name to identify the token easily</Paragraph>
|
||||
<Form.Item
|
||||
name="name"
|
||||
style={{marginTop: "-10px"}}
|
||||
rules={[{
|
||||
required: true,
|
||||
message: 'Please add a name for this personal access token',
|
||||
whitespace: true
|
||||
}]}
|
||||
>
|
||||
<Input
|
||||
placeholder={"for example \"Infra token\""}
|
||||
ref={inputNameRef}
|
||||
autoComplete="off"/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={24} style={{textAlign: "left"}}>
|
||||
<Paragraph style={{fontSize: "16px", fontWeight: "500"}}>Expires in</Paragraph>
|
||||
<Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-18px"}}>Number of days this token is valid for</Paragraph>
|
||||
<Form.Item
|
||||
name="expires_in"
|
||||
style={{marginTop: "-10px"}}
|
||||
rules={[{
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: 365,
|
||||
message: 'The expiration should be set between 1 and 365 days'
|
||||
}]}>
|
||||
<InputNumber addonAfter=" Days" style={{maxWidth: "150px"}}/>
|
||||
</Form.Item>
|
||||
<Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-18px"}}>Should be between 1 and 365 days</Paragraph>
|
||||
</Col>
|
||||
{/*<Col span={24}>*/}
|
||||
{/* <Button icon={<QuestionCircleFilled/>} type="link" target="_blank" disabled={true} style={{marginTop: "20px", marginBottom: "20px"}}*/}
|
||||
{/* href="https://netbird.io/docs/overview/personal-access-tokens">Learn more about personal access tokens</Button>*/}
|
||||
{/*</Col>*/}
|
||||
</Row>
|
||||
</Form>}
|
||||
{showPlainToken &&
|
||||
<Input style={{marginTop: "-15px", marginBottom: "25px"}} suffix={
|
||||
!tokenCopied ? <Button type="text" size="middle" className="btn-copy-code" icon={<CopyOutlined/>}
|
||||
style={{color: "rgb(107, 114, 128)", marginTop: "-1px"}}
|
||||
onClick={() => onCopyClick(plainToken, true)}/>
|
||||
: <Button type="text" size="middle" className="btn-copy-code" icon={<CheckOutlined/>}
|
||||
style={{color: "green", marginTop: "-1px"}}/>
|
||||
}
|
||||
defaultValue={plainToken}
|
||||
readOnly={true}
|
||||
></Input>}
|
||||
</Container>
|
||||
</Modal>
|
||||
{confirmModalContextHolder}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddPATPopup
|
||||
Loaded 20 of 76 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user