Compare commits

...
9 Commits
Author SHA1 Message Date
Misha Bragin 3a8106c1e7 Track block/unblock user activity (#181) 2023-05-17 09:53:46 +02:00
Misha Bragin b595e0d6a8 Switch to activate/deactivate a user (#179)
This PR adds a switch to deactivate (or block) a user.
Only admins can block/unblock users.
Users can't block themselves.
2023-05-15 16:57:35 +02:00
pascal-fischer 33621cae5d remove href- from breadcrumbs (#180) 2023-05-15 14:28:14 +02:00
Maycon Santos 360d807008 Enable creating service user for all domains (#178)
Removed unnecessary check from create button.
2023-05-14 11:56:14 +02:00
Misha Bragin 6f8897ffa5 Make breadcrumbs look like links (#176) 2023-05-10 18:48:11 +02:00
pascal-fischer 75d5f804c5 fix dropdown button in lists to only show ellipsis (#175) 2023-05-10 16:45:47 +02:00
pascal-fischer f7cac02a2d reduce api calls on user edit (#174) 2023-05-10 16:44:43 +02:00
Misha Bragin ec40730cb2 Fix setup key layout (#172) 2023-05-08 17:49:27 +02:00
Misha Bragin 7f3648861b Fix popups layout (#171) 2023-05-08 12:30:53 +02:00
16 changed files with 543 additions and 441 deletions

No files matched your search

File diff suppressed because it is too large. Load diff
+163 -107
View File
@@ -1,7 +1,7 @@
import {
Badge,
Breadcrumb,
Button,
Button, Card,
Col,
Divider,
Form,
@@ -10,7 +10,7 @@ import {
Row,
Select,
Skeleton,
Space, Table,
Space, Switch, Table,
Tag, Typography
} from "antd";
import {useDispatch, useSelector} from "react-redux";
@@ -33,6 +33,7 @@ import Column from "antd/lib/table/Column";
import {useOidcUser} from "@axa-fr/react-oidc";
const {Option} = Select;
const {Meta} = Card;
const {Title, Paragraph, Text} = Typography;
interface TokenDataTable extends PersonalAccessToken {
@@ -62,6 +63,7 @@ const UserEdit = () => {
const [formUser, setFormUser] = useState({} as FormUser)
const [form] = Form.useForm()
const [isAdmin, setIsAdmin] = useState(false);
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
@@ -86,10 +88,20 @@ const UserEdit = () => {
name: values.name,
groupsToCreate: groupsToCreate,
auto_groups: autoGroups,
is_service_user: isServiceUser
is_service_user: isServiceUser,
is_blocked: !values.is_active
} as UserToSave
}
useEffect(() => {
if(users) {
let currentUser = users.find((user) => user.is_current)
if(currentUser) {
setIsAdmin(currentUser.role === "admin");
}
}
}, [users])
const handleFormSubmit = () => {
form.validateFields()
.then((values) => {
@@ -101,18 +113,6 @@ const UserEdit = () => {
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)
@@ -208,14 +208,15 @@ const UserEdit = () => {
}
const transformTokenTable = (d: PersonalAccessToken[]): TokenDataTable[] => {
if(!d) {
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))
...p
} as TokenDataTable))
}
const getEmail = (token: PersonalAccessToken): string => {
@@ -271,8 +272,10 @@ const UserEdit = () => {
useEffect(() => {
if (user.is_current || user.is_service_user) {
dispatch(personalAccessTokenActions.getPersonalAccessTokens.request({getAccessTokenSilently: getTokenSilently,
payload: user.id}))
dispatch(personalAccessTokenActions.getPersonalAccessTokens.request({
getAccessTokenSilently: getTokenSilently,
payload: user.id
}))
}
}, [user])
@@ -282,29 +285,13 @@ const UserEdit = () => {
name: user.name,
role: user.role,
email: user.email,
is_blocked: user.is_blocked,
is_active: !user.is_blocked,
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"
@@ -316,10 +303,10 @@ const UserEdit = () => {
<Breadcrumb style={{marginBottom: "30px"}}
items={[
{
title: <Text onClick={() => onBreadcrumbUsersClick("Users")}>All Users</Text>,
title: <a onClick={() => onBreadcrumbUsersClick("Users")}>All Users</a>,
},
{
title: <Text onClick={() => onBreadcrumbUsersClick(tab)}>{tab}</Text>,
title: <a onClick={() => onBreadcrumbUsersClick(tab)}>{tab}</a>,
// menu: { items: menuItems },
},
{
@@ -327,35 +314,43 @@ const UserEdit = () => {
},
]}
/>
<Container style={{backgroundColor: "white", padding: "20px", borderRadius: "4px", boxSizing: "border-box", border: "0.5px solid #D9D9D9", marginBottom: "7px"}}>
<Card
bordered={true}
title={user.name}
loading={loading}
style={{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,
is_blocked: formUser.is_blocked,
is_active: !formUser.is_blocked,
autoGroupsNames: formUser.autoGroupsNames,
}}
>
<Row style={{ paddingBottom: "15px"}}>
{!user.is_service_user && <Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
<Form.Item
name="email"
label={<Text style={{fontSize: "16px", fontWeight: "500"}}>Email</Text>}
style={{marginRight: "70px"}}
>
<Input
disabled={user.id}
value={formUser.email}
style={{color: "#5a5c5a"}}
autoComplete="off"/>
</Form.Item>
</Col>}
<Row style={{paddingBottom: "15px"}}>
{!user.is_service_user &&
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
<Form.Item
name="email"
label={<Text style={{}}>Email</Text>}
style={{marginRight: "70px"}}
>
<Input
disabled={user.id}
value={formUser.email}
style={{color: "#5a5c5a"}}
autoComplete="off"/>
</Form.Item>
</Col>}
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
<Form.Item
name="role"
label={<Text style={{fontSize: "16px", fontWeight: "500"}}>Role</Text>}
label={<Text style={{}}>Role</Text>}
style={{marginRight: "50px"}}
>
<Select style={{width: '100%'}}
@@ -366,30 +361,45 @@ const UserEdit = () => {
</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)}
{!user.is_service_user && <Row style={{paddingBottom: "15px"}}>
<Col xs={24} sm={24} md={11} lg={11} xl={11} xxl={11} span={11}>
<Form.Item
name="autoGroupsNames"
label={<Text style={{}}>Auto-assigned groups</Text>}
tooltip="Every peer enrolled with this user will be automatically added to these groups"
rules={[{validator: selectValidator}]}
style={{marginRight: "70px"}}
>
{
tagGroups.map(m =>
<Option key={m}>{optionRender(m)}</Option>
)
}
</Select>
<Select mode="tags"
placeholder="Associate groups with the user"
tagRender={tagRender}
dropdownRender={dropDownRender}
disabled={oidcUser && !isUserAdmin(oidcUser.sub)}
>
{
tagGroups.map(m =>
<Option key={m}>{optionRender(m)}</Option>
)
}
</Select>
</Form.Item>
</Col>
{!user.is_current && isAdmin && (
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
<Form.Item
valuePropName="checked"
name="is_active"
label="Active"
style={{marginRight: "50px"}}
>
<Switch/>
</Form.Item>
</Col>
</Col>)}
</Row>}
<Space style={{display: 'flex', justifyContent: 'start'}}>
<Button disabled={loading} onClick={onCancel}>Cancel</Button>
<Button type="primary"
@@ -397,16 +407,27 @@ const UserEdit = () => {
</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"}}>
</Card>
{user && (user.is_current || user.is_service_user) && <Card
bordered={true}
loading={loading}
style={{marginBottom: "7px"}}
>
<div style={{maxWidth: "800px"}}>
<Paragraph style={{textAlign: "left", whiteSpace: "pre-line", fontSize: "1.5em"}}>Access tokens</Paragraph>
<Row gutter={21} style={{marginTop: "-22px", marginBottom: "10px"}}>
<Paragraph
style={{textAlign: "left", whiteSpace: "pre-line", fontSize: "16px", fontWeight: "bold"}}>Access
tokens</Paragraph>
<Row gutter={21} style={{marginTop: "-16px", marginBottom: "10px"}}>
<Col xs={24} sm={24} md={20} lg={20} xl={20} xxl={20} span={20}>
<Paragraph type={"secondary"} style={{textAlign: "left", whiteSpace: "pre-line"}}>Access tokens give access to the Netbird API</Paragraph>
<Paragraph type={"secondary"}
style={{textAlign: "left", whiteSpace: "pre-line"}}>
Access tokens give access to NetBird API</Paragraph>
</Col>
<Col xs={24} sm={24} md={1} lg={1} xl={1} xxl={1} span={1} style={{marginTop: "-16px"}}>
{personalAccessTokens && personalAccessTokens.length > 0 && <Button type="primary" onClick={onClickAddNewPersonalAccessToken}>Create Token</Button>}
{personalAccessTokens && personalAccessTokens.length > 0 &&
<Button type="primary" onClick={onClickAddNewPersonalAccessToken}>Create
token</Button>}
</Col>
</Row>
{personalAccessTokens && personalAccessTokens.length > 0 &&
@@ -418,34 +439,62 @@ const UserEdit = () => {
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))}
<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"}}/>
<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>
<Paragraph style={{
margin: "0px",
padding: "0px"
}}>{(record as TokenDataTable).name}</Paragraph>
<Paragraph type={"secondary"} style={{
fontSize: "13px",
fontWeight: "400",
margin: "0px",
marginTop: "-2px",
padding: "0px"
}}>{"Created" + ((record as TokenDataTable).created_by_email && user.is_service_user ? " by " + (record as TokenDataTable).created_by_email : "") + " on " + fullDate((record as TokenDataTable).created_at)}</Paragraph>
</Col>
</Row>
</>)
</>)
}}/>
<Column render={(text, record, index) => {
return <>
<Paragraph type={"secondary"} style={{textAlign: "left", fontSize: "11px"}}>Expires on</Paragraph>
<Paragraph type={"secondary"} style={{textAlign: "left", marginTop: "-10px", marginBottom: "0", fontSize: "15px"}}>{fullDate((record as TokenDataTable).expiration_date)}</Paragraph>
</>
}}
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>
</>
}}
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) => {
@@ -460,16 +509,23 @@ const UserEdit = () => {
/>
</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>}
{(personalAccessTokens === null || personalAccessTokens.length === 0) &&
<Space direction="vertical" size="small" align="start"
style={{
display: 'flex',
padding: '35px 0px',
marginTop: "-40px",
justifyContent: 'center'
}}>
<Paragraph
style={{textAlign: "start", whiteSpace: "pre-line"}}>
You don’t have any access tokens yet
</Paragraph>
<Button type="primary" onClick={onClickAddNewPersonalAccessToken}>Create token</Button>
</Space>}
</div>
</Container>}
</Card>}
</div>
<AddPATPopup/>
{confirmModalContextHolder}
+12 -14
View File
@@ -130,27 +130,25 @@ const AddPATPopup = () => {
}
width={460}
>
<Container style={{textAlign: "start"}}>
<Container style={{textAlign: "start", marginLeft: "-15px", marginRight: "-15px"}}>
<Paragraph
style={{textAlign: "start", whiteSpace: "pre-line", fontSize: "22px", fontWeight: "500"}}>
{showPlainToken ? "Token created successfully!" : "Create a Personal Access Token"}
style={{textAlign: "start", whiteSpace: "pre-line", fontSize: "22px"}}>
{showPlainToken ? "Token created successfully!" : "Create 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."}
{"Use this token to access 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>}
marginTop: "25px",
}}>{"This token won't be shown again, so be sure to copy and store it in a secure location"}</Paragraph>}
{!showPlainToken && <Form layout="vertical" hideRequiredMark form={form}
initialValues={{
expires_in: ExpiresInDefault,
@@ -160,14 +158,14 @@ const AddPATPopup = () => {
<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>
<Paragraph style={{fontWeight: "bold", marginTop: "-10px"}}>Name</Paragraph>
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Set an easily identifiable name for your token</Paragraph>
<Form.Item
name="name"
style={{marginTop: "-10px"}}
rules={[{
required: true,
message: 'Please add a name for this personal access token',
message: 'Please add a name for this token',
whitespace: true
}]}
>
@@ -179,9 +177,9 @@ const AddPATPopup = () => {
</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>
<Col span={24} style={{textAlign: "left", marginTop: "10px"}}>
<Paragraph style={{fontWeight: "bold", marginTop: "-10px"}}>Expires in</Paragraph>
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Number of days this token will be valid for</Paragraph>
<Form.Item
name="expires_in"
style={{marginTop: "-10px"}}
+11 -11
View File
@@ -199,10 +199,10 @@ const AddServiceUserPopup = () => {
}
width={460}
>
<Container style={{textAlign: "start"}}>
<Container style={{textAlign: "start", marginLeft: "-15px", marginRight: "-15px"}}>
<Paragraph
style={{textAlign: "start", whiteSpace: "pre-line", fontSize: "22px", fontWeight: "500"}}>
{"Add Service User"}
style={{textAlign: "start", whiteSpace: "pre-line", fontSize: "22px"}}>
{"Add service user"}
</Paragraph>
<Paragraph type={"secondary"}
style={{
@@ -211,7 +211,7 @@ const AddServiceUserPopup = () => {
marginTop: "-23px",
paddingBottom: "25px",
}}>
{"Service Users are non-login users that are not associated with any specific person."}
{"Service users are non-login users that are not associated with any specific person."}
</Paragraph>
<Form layout="vertical" hideRequiredMark form={form}
initialValues={{
@@ -220,8 +220,8 @@ const AddServiceUserPopup = () => {
>
<Row gutter={16}>
<Col span={24}>
<Paragraph style={{fontSize: "16px", fontWeight: "500", marginTop: "-10px"}}>Name</Paragraph>
<Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-18px"}}>Set a name to easily identify the user</Paragraph>
<Paragraph style={{fontWeight: "bold", marginTop: "-10px"}}>Name</Paragraph>
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Set a name to easily identify the user</Paragraph>
<Form.Item
name="name"
rules={[{
@@ -232,14 +232,14 @@ const AddServiceUserPopup = () => {
style={{marginTop: "-8px"}}
>
<Input
placeholder={'for example "Ansible User"'}
placeholder={'for example "Ansible user"'}
ref={inputNameRef}
autoComplete="off"/>
</Form.Item>
</Col>
<Col span={24}>
<Paragraph style={{fontSize: "16px", fontWeight: "500", marginTop: "0px"}}>Role</Paragraph>
<Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-18px"}}>Set a role for the user to assign dashboard access permissions</Paragraph>
<Paragraph style={{fontWeight: "bold", marginTop: "0px"}}>Role</Paragraph>
<Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-15px"}}>Set a role for the user to assign access permissions</Paragraph>
<Form.Item
name="role"
rules={[{
@@ -256,8 +256,8 @@ const AddServiceUserPopup = () => {
</Form.Item>
</Col>
{/*<Col span={24}>*/}
{/* <Paragraph style={{fontSize: "16px", fontWeight: "500", marginTop: "0px"}}>Auto-assigned groups</Paragraph>*/}
{/* <Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-18px"}}>Add groups, that will be assigned to peers added by this user</Paragraph>*/}
{/* <Paragraph style={{fontWeight: "bold", marginTop: "0px"}}>Auto-assigned groups</Paragraph>*/}
{/* <Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-15px"}}>Add groups, that will be assigned to peers added by this user</Paragraph>*/}
{/* <Form.Item*/}
{/* name="autoGroupsNames"*/}
{/* label="Auto-assigned groups"*/}
+10 -12
View File
@@ -12,7 +12,6 @@ import {
Typography
} from "antd";
import {Container} from "../Container";
import {CloseOutlined} from "@ant-design/icons";
import React, {useEffect, useRef, useState} from "react";
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "typesafe-actions";
@@ -20,7 +19,6 @@ import {useGetTokenSilently} from "../../utils/token";
import {actions as userActions} from "../../store/user";
import {actions as groupActions} from "../../store/group";
import {User, UserToSave} from "../../store/user/types";
import {Header} from "antd/es/layout/layout";
import {RuleObject} from "antd/lib/form";
import {CustomTagProps} from "rc-select/lib/BaseSelect";
@@ -200,10 +198,10 @@ const InviteUserPopup = () => {
}
width={460}
>
<Container style={{textAlign: "start"}}>
<Container style={{textAlign: "start", marginLeft: "-15px", marginRight: "-15px"}}>
<Paragraph
style={{textAlign: "start", whiteSpace: "pre-line", fontSize: "22px", fontWeight: "500"}}>
{"Invite User"}
{"Invite user"}
</Paragraph>
<Paragraph type={"secondary"}
style={{
@@ -222,8 +220,8 @@ const InviteUserPopup = () => {
>
<Row gutter={16}>
<Col span={24}>
<Paragraph style={{fontSize: "16px", fontWeight: "500", marginTop: "-10px"}}>Name</Paragraph>
<Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-18px"}}>Set a name to easily identify the user</Paragraph>
<Paragraph style={{fontWeight: "bold", marginTop: "-10px"}}>Name</Paragraph>
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Set a name to easily identify the user</Paragraph>
<Form.Item
name="name"
rules={[{
@@ -240,8 +238,8 @@ const InviteUserPopup = () => {
</Form.Item>
</Col>
<Col span={24}>
<Paragraph style={{fontSize: "16px", fontWeight: "500", marginTop: "0px"}}>Email</Paragraph>
<Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-18px"}}>Provide the email address of the user</Paragraph>
<Paragraph style={{ fontWeight: "bold", marginTop: "0px"}}>Email</Paragraph>
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Provide the email address of the user</Paragraph>
<Form.Item
name="email"
rules={[{
@@ -259,8 +257,8 @@ const InviteUserPopup = () => {
</Form.Item>
</Col>
<Col span={24}>
<Paragraph style={{fontSize: "16px", fontWeight: "500", marginTop: "0px"}}>Role</Paragraph>
<Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-18px"}}>Set a role for the user to assign dashboard access permissions</Paragraph>
<Paragraph style={{ fontWeight: "bold", marginTop: "0px"}}>Role</Paragraph>
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Set a role for the user to assign access permissions</Paragraph>
<Form.Item
name="role"
rules={[{
@@ -277,8 +275,8 @@ const InviteUserPopup = () => {
</Form.Item>
</Col>
<Col span={24}>
<Paragraph style={{fontSize: "16px", fontWeight: "500", marginTop: "0px"}}>Auto-assigned groups</Paragraph>
<Paragraph type={"secondary"} style={{fontSize: "14px", marginTop: "-18px"}}>Add groups, that will be assigned to peers added by this user</Paragraph>
<Paragraph style={{fontWeight: "bold", marginTop: "0px"}}>Auto-assigned groups</Paragraph>
<Paragraph type={"secondary"} style={{marginTop: "-15px"}}>Add groups, that will be assigned to peers added by this user</Paragraph>
<Form.Item
name="autoGroupsNames"
tooltip="Every peer enrolled with this user will be automatically added to these groups"
+2 -12
View File
@@ -5,8 +5,6 @@ import service from './service';
import actions from './actions';
import serviceGroup from "../group/service";
import {Group} from "../group/types";
import {actions as groupActions} from "../group";
import {PersonalAccessToken} from "../personal-access-token/types";
export function* getUsers(action: ReturnType<typeof actions.getUsers.request>): Generator {
try {
@@ -74,7 +72,8 @@ export function* saveUser(action: ReturnType<typeof actions.saveUser.request>):
email: userToSave.email,
role: userToSave.role,
auto_groups: newGroups,
is_service_user: userToSave.is_service_user
is_service_user: userToSave.is_service_user,
is_blocked: userToSave.is_blocked
} as UserToSave
let effect
@@ -99,15 +98,6 @@ export function* saveUser(action: ReturnType<typeof actions.saveUser.request>):
error: null,
data: response.body
} as CreateResponse<User | null>));
yield put(groupActions.getGroups.request({
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: null
}));
yield put(actions.getUsers.request({
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: null
}));
} catch (err) {
yield put(actions.saveUser.failure({
loading: false,
+2
View File
@@ -7,10 +7,12 @@ export interface User {
auto_groups: string[];
is_current?: boolean;
is_service_user?: boolean;
is_blocked?: boolean;
}
export interface FormUser extends User {
autoGroupsNames: string[]
is_active?: boolean
}
export interface UserToSave extends User {
+13 -6
View File
@@ -27,7 +27,7 @@ import {Rule} from "../store/rule/types";
import {actions as ruleActions} from "../store/rule";
import {actions as groupActions} from "../store/group";
import {filter, sortBy} from "lodash";
import {CloseOutlined, ExclamationCircleOutlined} from "@ant-design/icons";
import {CloseOutlined, EllipsisOutlined, ExclamationCircleOutlined} from "@ant-design/icons";
import bidirect from '../assets/direct_bi.svg';
import inbound from '../assets/direct_in.svg';
import outbound from '../assets/direct_out.svg';
@@ -37,6 +37,7 @@ import AccessControlModalGroups from "../components/AccessControlModalGroups";
import tableSpin from "../components/Spin";
import {useGetTokenSilently} from "../utils/token";
import {usePageSizeHelpers} from "../utils/pageSize";
import {PeerDataTable} from "../store/peer/types";
const {Title, Paragraph, Text} = Typography;
const {Column} = Table;
@@ -449,11 +450,17 @@ export const AccessControl = () => {
<Column title="" align="center"
render={(text, record, index) => {
if (deletedRule.loading || savedRule.loading) return <></>
return <Dropdown.Button type="text" overlay={actionsMenu}
trigger={["click"]}
onOpenChange={visible => {
if (visible) setRuleToAction(record as RuleDataTable)
}}></Dropdown.Button>
return (
<Dropdown trigger={["click"]} overlay={actionsMenu} onOpenChange={visible => {
if (visible) setRuleToAction(record as RuleDataTable)
}}>
<Button type="text">
<Space>
<EllipsisOutlined />
</Space>
</Button>
</Dropdown>
)
}}
/>
</Table>
+2
View File
@@ -212,6 +212,8 @@ export const Activity = () => {
case "service.user.delete":
return renderMultiRowSpan(event.meta.name,"Service User")
case "user.invite":
case "user.block":
case "user.unblock":
if (user) {
return renderMultiRowSpan(user.name ? user.name : user.id,user.email ? user.email : "User")
}
+11 -6
View File
@@ -31,7 +31,7 @@ import {Group} from "../store/group/types";
import {TooltipPlacement} from "antd/es/tooltip";
import {NameServer, NameServerGroup} from "../store/nameservers/types";
import NameServerGroupUpdate from "../components/NameServerGroupUpdate";
import {ExclamationCircleOutlined} from "@ant-design/icons";
import {EllipsisOutlined, ExclamationCircleOutlined} from "@ant-design/icons";
import {useGetGroupTagHelpers} from "../utils/groups";
import {usePageSizeHelpers} from "../utils/pageSize";
@@ -441,11 +441,16 @@ export const Nameservers = () => {
<Column title="" align="center" width="30px"
render={(text, record) => {
return (
<Dropdown.Button type="text" overlay={actionsMenu}
trigger={["click"]}
onOpenChange={visible => {
if (visible) setNsGroupToAction(record as NameserverGroupDataTable)
}}></Dropdown.Button>)
<Dropdown trigger={["click"]} overlay={actionsMenu} onOpenChange={visible => {
if (visible) setNsGroupToAction(record as NameserverGroupDataTable)
}}>
<Button type="text">
<Space>
<EllipsisOutlined />
</Space>
</Button>
</Dropdown>
)
}}
/>
</Table>
+12 -6
View File
@@ -6,7 +6,7 @@ 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 {EllipsisOutlined, ExclamationCircleOutlined, ReloadOutlined,} from '@ant-design/icons';
import {
Alert,
Button,
@@ -611,11 +611,17 @@ export const Peers = () => {
<Column title="Version" dataIndex="version"/>
<Column title="" align="center"
render={(text, record, index) => {
return <Dropdown.Button type="text" overlay={actionsMenu}
trigger={["click"]}
onOpenChange={visible => {
if (visible) setPeerToAction(record as PeerDataTable)
}}></Dropdown.Button>
return (
<Dropdown trigger={["click"]} overlay={actionsMenu} onOpenChange={visible => {
if (visible) setPeerToAction(record as PeerDataTable)
}}>
<Button type="text">
<Space>
<EllipsisOutlined />
</Space>
</Button>
</Dropdown>
)
}}
/>
</Table>)}
+105 -40
View File
@@ -1,4 +1,4 @@
import React, {useEffect, 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';
@@ -8,29 +8,33 @@ import {
Button,
Card,
Col,
Dropdown,
Dropdown, Empty,
Input,
Menu,
message, Modal,
Popover,
Row,
Select,
Space,
Space, Switch,
Table,
Tag,
Tag, Tooltip,
Typography,
} from "antd";
import {User} from "../store/user/types";
import {User, UserToSave} from "../store/user/types";
import {filter} from "lodash";
import tableSpin from "../components/Spin";
import {useGetTokenSilently} from "../utils/token";
import {actions as groupActions} from "../store/group";
import {Group} from "../store/group/types";
import {TooltipPlacement} from "antd/es/tooltip";
import {isLocalDev, isNetBirdHosted} from "../utils/common";
import {capitalize, isLocalDev, isNetBirdHosted} from "../utils/common";
import {usePageSizeHelpers} from "../utils/pageSize";
import AddServiceUserPopup from "../components/popups/AddServiceUserPopup";
import InviteUserPopup from "../components/popups/InviteUserPopup";
import {Peer, PeerDataTable} from "../store/peer/types";
import {ExclamationCircleOutlined, MinusOutlined} from "@ant-design/icons";
import {actions as peerActions} from "../store/peer";
import {useOidcUser} from "@axa-fr/react-oidc";
const {Title, Paragraph, Text} = Typography;
const {Column} = Table;
@@ -42,10 +46,12 @@ interface UserDataTable extends User {
const styleNotification = {marginTop: 85}
export const RegularUsers = () => {
const {onChangePageSize,pageSizeOptions,pageSize} = usePageSizeHelpers()
const {onChangePageSize, pageSizeOptions, pageSize} = usePageSizeHelpers()
const {getTokenSilently} = useGetTokenSilently()
const dispatch = useDispatch()
const [isAdmin, setIsAdmin] = useState(false);
const groups = useSelector((state: RootState) => state.group.data)
const users = useSelector((state: RootState) => state.user.regularUsers);
const failed = useSelector((state: RootState) => state.user.failed);
@@ -114,6 +120,7 @@ export const RegularUsers = () => {
auto_groups: userToAction?.auto_groups ? userToAction?.auto_groups : [],
name: userToAction?.name,
role: userToAction?.role,
is_blocked: userToAction?.is_blocked,
} as User));
}
@@ -122,6 +129,15 @@ export const RegularUsers = () => {
dispatch(userActions.setUser(null as unknown as User));
}
useEffect(() => {
if(users) {
let currentUser = users.find((user) => user.is_current)
if(currentUser) {
setIsAdmin(currentUser.role === "admin");
}
}
}, [users])
const renderPopoverGroups = (label: string, rowGroups: string[] | string[] | null, userToAction: UserDataTable) => {
let groupsMap = new Map<string, Group>();
@@ -196,10 +212,10 @@ export const RegularUsers = () => {
let errorMsg = "Failed to update user"
switch (savedUser.error.statusCode) {
case 412:
errorMsg = savedUser.error.data
break
case 403:
errorMsg = "Failed to update user. You might not have enough permissions."
if (savedUser.error.data) {
errorMsg = capitalize(savedUser.error.data.message)
}
break
}
message.error({
@@ -243,16 +259,59 @@ export const RegularUsers = () => {
name: user.name,
is_current: user.is_current,
is_service_user: user.is_service_user,
is_blocked: user.is_blocked
} as User));
dispatch(userActions.setEditUserPopupVisible(true));
}
const handleBlockUser = (block: boolean, user: UserDataTable) => {
if (block) {
confirmModal.confirm({
icon: <ExclamationCircleOutlined/>,
title: "Are you sure you want to deactivate \"" + user.name + "\"?",
width: 600,
content: <Space direction="vertical" size="small">
<Paragraph>Disabling this user will disconnect their devices and block dashboard access.</Paragraph>
</Space>,
onOk() {
let userToSave = createUserToSave(user, block)
dispatch(userActions.saveUser.request({
getAccessTokenSilently: getTokenSilently,
payload: userToSave
}));
},
onCancel() {
// noop
},
});
} else {
let userToSave = createUserToSave(user, block)
dispatch(userActions.saveUser.request({
getAccessTokenSilently: getTokenSilently,
payload: userToSave
}));
}
}
const createUserToSave = (values: UserDataTable, block: boolean): UserToSave => {
return {
id: values.id,
role: values.role,
name: values.name,
groupsToCreate: Array.of(),
auto_groups: values.auto_groups,
is_service_user: values.is_service_user,
is_blocked: block
} as UserToSave
}
return (
<>
<Container style={{padding: "0px"}}>
<Row>
<Col span={24}>
<Paragraph>Manage users and their permissions.{(window.location.hostname == "app.netbird.io") ? "Same-domain email users are added automatically on first sign-in." : ""}</Paragraph>
<Paragraph>Manage users and their
permissions.{(window.location.hostname == "app.netbird.io") ? "Same-domain email users are added automatically on first sign-in." : ""}</Paragraph>
<Space direction="vertical" size="large" style={{display: 'flex'}}>
<Row gutter={[16, 24]}>
<Col xs={24} sm={24} md={8} lg={8} xl={8} xxl={8} span={8}>
@@ -272,15 +331,16 @@ export const RegularUsers = () => {
xl={5}
xxl={5} span={5}>
{(isNetBirdHosted() || isLocalDev()) &&
<Row justify="end">
<Col>
<Button type="primary" onClick={onClickInviteUser}>Invite User</Button>
</Col>
</Row>}
<Row justify="end">
<Col>
<Button type="primary" onClick={onClickInviteUser}>Invite user</Button>
</Col>
</Row>}
</Col>
</Row>
{failed &&
<Alert message={failed.message} description={failed.data ? failed.data.message : " "} type="error" showIcon
<Alert message={failed.message} description={failed.data ? failed.data.message : " "}
type="error" showIcon
closable/>
}
<Card bodyStyle={{padding: 0}}>
@@ -305,6 +365,7 @@ export const RegularUsers = () => {
className="tooltip-label">
<Text
strong>{(text && text.trim() !== "") ? text : (record as User).id}</Text>
</Button>
if ((record as User).is_current) {
@@ -313,25 +374,18 @@ export const RegularUsers = () => {
</div>
}
if ((record as User).status === "invited") {
return <div>{btn}
<Tag color="gold">invited</Tag>
</div>
}
return btn
}}
/>
<Column title="Name" dataIndex="name"
onFilter={(value: string | number | boolean, record) => (record as any).name.includes(value)}
sorter={(a, b) => ((a as any).name.localeCompare((b as any).name))}/>
<Column title="Status" dataIndex="status"
align="center"
onFilter={(value: string | number | boolean, record) => (record as any).status.includes(value)}
sorter={(a, b) => ((a as any).status.localeCompare((b as any).status))}
render={(text, record, index) => {
if (text == "active") {
return <Tag color="green">{text}</Tag>
} else if (text === "invited"){
return <Tag color="gold">{text}</Tag>
}
return <Tag color="red">{text}</Tag>
}}
/>
<Column title="Groups" dataIndex="groupsCount" align="center"
render={(text, record: UserDataTable, index) => {
return renderPopoverGroups(text, record.auto_groups, record)
@@ -340,16 +394,27 @@ export const RegularUsers = () => {
<Column title="Role" dataIndex="role"
onFilter={(value: string | number | boolean, record) => (record as any).role.includes(value)}
sorter={(a, b) => ((a as any).role.localeCompare((b as any).role))}/>
{/*<Column title="" align="center" width="30px"*/}
{/* render={(text, record, index) => {*/}
{/* return (*/}
{/* <Dropdown.Button type="text" overlay={actionsMenu}*/}
{/* trigger={["click"]}*/}
{/* onVisibleChange={visible => {*/}
{/* if (visible) setUserToAction(record as UserDataTable)*/}
{/* }}></Dropdown.Button>)*/}
{/* }}*/}
{/*/>*/}
{isAdmin && (
<Column title="Active" align="center" width="150px" dataIndex="is_blocked"
render={(e, record: UserDataTable, index) => {
let witch = <Switch size={"small"} checked={!e}
disabled={record.is_current}
onClick={(active: boolean) => {
handleBlockUser(!active, record)
}}
/>
if (record.is_current) {
return <Tooltip
title="You can't block or unblock yourself">
<Empty image={""} description={""} style={{height: "1px", width: "auto"}}/>
</Tooltip>
}
return witch
}}
/>
)}
</Table>
</Card>
</Space>
+12 -5
View File
@@ -28,7 +28,7 @@ import {Route, RouteToSave} from "../store/route/types";
import {actions as routeActions} from "../store/route";
import {actions as peerActions} from "../store/peer";
import {filter, sortBy} from "lodash";
import {ExclamationCircleOutlined, QuestionCircleOutlined} from "@ant-design/icons";
import {EllipsisOutlined, ExclamationCircleOutlined, QuestionCircleOutlined} from "@ant-design/icons";
import RouteUpdate from "../components/RouteUpdate";
import tableSpin from "../components/Spin";
import {
@@ -410,10 +410,17 @@ export const Routes = () => {
<Column title="" align="center"
render={(text, record) => {
if (deletedRoute.loading || savedRoute.loading) return <></>
return <Dropdown.Button type="text" overlay={actionsMenu} trigger={["click"]}
onOpenChange={visible => {
if (visible) setRouteToAction(record as RouteDataTable)
}}></Dropdown.Button>
return (
<Dropdown trigger={["click"]} overlay={actionsMenu} onOpenChange={visible => {
if (visible) setRouteToAction(record as RouteDataTable)
}}>
<Button type="text">
<Space>
<EllipsisOutlined />
</Space>
</Button>
</Dropdown>
)
}}
/>
</Table>
+5 -6
View File
@@ -22,7 +22,7 @@ import {filter} from "lodash";
import tableSpin from "../components/Spin";
import {useGetTokenSilently} from "../utils/token";
import {actions as groupActions} from "../store/group";
import {isLocalDev, isNetBirdHosted} from "../utils/common";
import {capitalize, isLocalDev, isNetBirdHosted} from "../utils/common";
import {usePageSizeHelpers} from "../utils/pageSize";
import AddServiceUserPopup from "../components/popups/AddServiceUserPopup";
import {ExclamationCircleOutlined} from "@ant-design/icons";
@@ -111,10 +111,10 @@ export const ServiceUsers = () => {
let errorMsg = "Failed to update user"
switch (savedUser.error.statusCode) {
case 412:
errorMsg = savedUser.error.data
break
case 403:
errorMsg = "Failed to update user. You might not have enough permissions."
if (savedUser.error.data) {
errorMsg = capitalize(savedUser.error.data.message)
}
break
}
message.error({
@@ -187,12 +187,11 @@ export const ServiceUsers = () => {
lg={5}
xl={5}
xxl={5} span={5}>
{(isNetBirdHosted() || isLocalDev()) &&
<Row justify="end">
<Col>
<Button type="primary" onClick={onClickCreateServiceUser}>Create Service User</Button>
</Col>
</Row>}
</Row>
</Col>
</Row>
{failed &&
+6 -6
View File
@@ -372,7 +372,7 @@ export const SetupKeys = () => {
<Row justify="end">
<Col>
<Button type="primary" onClick={onClickAddNewSetupKey}>
Add Key
Add key
</Button>
</Col>
</Row>
@@ -418,7 +418,7 @@ export const SetupKeys = () => {
color: "#252526",
}}
>
Create Setup Key
Create setup key
</Text>
</Col>
<Col style={{ marginTop: "17px" }}>
@@ -429,8 +429,8 @@ export const SetupKeys = () => {
lineHeight: "22px",
}}
>
Manage Setup Keys to register new machines in your network. The key
links the machine to an account during initial setup.
Manage setup keys to register new machines in your network. The key
links machines to your account during initial setup.
<a
target="_blank"
rel="noreferrer"
@@ -457,7 +457,7 @@ export const SetupKeys = () => {
}}
onClick={onClickAddNewSetupKey}
>
Add Key
Add key
</Button>
</Col>
</Container>
@@ -592,4 +592,4 @@ export const SetupKeys = () => {
);
};
export default SetupKeys;
export default SetupKeys;
-1
View File
@@ -2,7 +2,6 @@ import React, {useEffect, useState} from 'react';
import {Col, Row, Tabs, TabsProps} from "antd";
import {Container} from "../components/Container";
import {useOidcUser} from "@axa-fr/react-oidc";
import {actions as userActions} from "../store/user";
import {useGetTokenSilently} from "../utils/token";
import {useDispatch, useSelector} from "react-redux";