Compare commits

...
Author SHA1 Message Date
Misha Bragin 14d9b80029 Limit UI view for the "user" role (#259) 2023-08-15 22:36:21 +02:00
5 changed files with 293 additions and 209 deletions

No files matched your search

+1
View File
@@ -53,6 +53,7 @@ const Navbar = () => {
"/acls",
"/routes",
"/dns",
"/users",
"/activity",
"/settings",
];
+3 -2
View File
@@ -166,6 +166,7 @@ const UserEdit = (props: any) => {
dispatch(userActions.setUser(null as unknown as User));
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null));
dispatch(userActions.setUserTabOpen(key));
dispatch(userActions.setEditUserPopupVisible(false));
};
const selectValidator = (_: RuleObject, value: string[]) => {
@@ -331,7 +332,7 @@ const UserEdit = (props: any) => {
// menu: { items: menuItems },
},
{
title: user.name,
title: user?.name,
},
]}
/>
@@ -386,7 +387,7 @@ const UserEdit = (props: any) => {
style={{ marginRight: "70px", fontWeight: "500" }}
>
<Input
disabled={user.id}
disabled={user?.id}
value={formUser.email}
style={{ color: "#8c8c8c" }}
autoComplete="off"
+4
View File
@@ -515,4 +515,8 @@ ul.ant-list-items {
.d-none{
display: none!important;
}
.nohover {
background: transparent!important;
cursor: text;
}
+204 -135
View File
@@ -1,198 +1,268 @@
import {all, call, spawn, put, select, takeLatest} from 'redux-saga/effects';
import { all, call, spawn, put, select, takeLatest } from "redux-saga/effects";
import {
ApiError,
ApiResponse,
ChangeResponse,
CreateResponse,
DeleteResponse
} from '../../services/api-client/types';
import { Peer } from './types'
import service from './service';
import actions from './actions';
import {Group, GroupPeer} from "../group/types";
DeleteResponse,
} from "../../services/api-client/types";
import { Peer } from "./types";
import service from "./service";
import actions from "./actions";
import { Group, GroupPeer } from "../group/types";
import serviceGroup from "../group/service";
import {actions as groupActions} from "../group";
export function* getPeers(action: ReturnType<typeof actions.getPeers.request>): Generator {
import { actions as groupActions } from "../group";
import userService from "../user/service";
export function* getPeers(
action: ReturnType<typeof actions.getPeers.request>
): Generator {
try {
yield put(
actions.setDeletePeer({
loading: false,
success: false,
failure: false,
error: null,
data: null,
} as DeleteResponse<string | null>)
);
yield put(actions.setDeletePeer({
loading: false,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>))
const users: any = yield call(userService.getUsers, action.payload);
let currentUser = users.body.find((user: any) => user.is_current);
const effect = yield call(service.getPeers, action.payload);
const response = effect as ApiResponse<Peer[]>;
yield put(actions.getPeers.success(response.body));
let peersBody: any;
if (currentUser.role === "user") {
peersBody = response.body.filter((pe) => {
return pe.user_id === currentUser.id;
});
} else {
peersBody = response.body;
}
yield put(actions.getPeers.success(peersBody));
} catch (err) {
yield put(actions.getPeers.failure(err as ApiError));
}
}
export function* setDeletePeer(action: ReturnType<typeof actions.setDeletePeer>): Generator {
yield put(actions.setDeletePeer(action.payload))
export function* setDeletePeer(
action: ReturnType<typeof actions.setDeletePeer>
): Generator {
yield put(actions.setDeletePeer(action.payload));
}
export function* deletePeer(action: ReturnType<typeof actions.deletedPeer.request>): Generator {
export function* deletePeer(
action: ReturnType<typeof actions.deletedPeer.request>
): Generator {
try {
yield call(actions.setDeletePeer,{
yield call(actions.setDeletePeer, {
loading: true,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>)
data: null,
} as DeleteResponse<string | null>);
const effect = yield call(service.deletedPeer, action.payload);
const response = effect as ApiResponse<any>;
yield put(actions.deletedPeer.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as DeleteResponse<string | null>));
yield put(
actions.deletedPeer.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body,
} as DeleteResponse<string | null>)
);
const peers = (yield select(state => state.peer.data)) as Peer[]
yield put(actions.getPeers.success(peers.filter((p:Peer) => p.id !== action.payload.payload)))
const peers = (yield select((state) => state.peer.data)) as Peer[];
yield put(
actions.getPeers.success(
peers.filter((p: Peer) => p.id !== action.payload.payload)
)
);
} catch (err) {
yield put(actions.deletedPeer.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null
} as DeleteResponse<string | null>));
yield put(
actions.deletedPeer.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null,
} as DeleteResponse<string | null>)
);
}
}
export function* saveGroups(action: ReturnType<typeof actions.saveGroups.request>): Generator {
export function* saveGroups(
action: ReturnType<typeof actions.saveGroups.request>
): Generator {
try {
yield put(actions.setSavedGroups({
loading: true,
success: false,
failure: false,
error: null,
data: null
}))
yield put(
actions.setSavedGroups({
loading: true,
success: false,
failure: false,
error: null,
data: null,
})
);
const currentGroups = [...(yield select(state => state.group.data)) as Group[]]
const currentGroups = [
...((yield select((state) => state.group.data)) as Group[]),
];
const peerGroupsToSave = action.payload.payload
const peerGroupsToSave = action.payload.payload;
let groupsToSave = [] as Group[]
let groupsNoId = [] as Group[]
let groupsToSave = [] as Group[];
let groupsNoId = [] as Group[];
groupsToSave = groupsToSave.concat(
currentGroups
.filter(g => peerGroupsToSave.groupsToRemove.includes(g.id || ''))
.map(g => ({
id: g.id,
name: g.name,
peers: (g.peers as GroupPeer[]).filter(p => p.id !== peerGroupsToSave.ID).map(p => p.id) as string[]
}))
)
currentGroups
.filter((g) => peerGroupsToSave.groupsToRemove.includes(g.id || ""))
.map((g) => ({
id: g.id,
name: g.name,
peers: (g.peers as GroupPeer[])
.filter((p) => p.id !== peerGroupsToSave.ID)
.map((p) => p.id) as string[],
}))
);
groupsToSave = groupsToSave.concat(
currentGroups
.filter(g => peerGroupsToSave.groupsToAdd.includes(g.id || ''))
.map(g => ({
id: g.id,
name: g.name,
Peers: g.peers ? [...(g.peers as GroupPeer[]).map((p:GroupPeer) => p.id), peerGroupsToSave.ID] : [peerGroupsToSave.ID]
}))
)
currentGroups
.filter((g) => peerGroupsToSave.groupsToAdd.includes(g.id || ""))
.map((g) => ({
id: g.id,
name: g.name,
Peers: g.peers
? [
...(g.peers as GroupPeer[]).map((p: GroupPeer) => p.id),
peerGroupsToSave.ID,
]
: [peerGroupsToSave.ID],
}))
);
groupsNoId = peerGroupsToSave.groupsNoId.map(g => ({
groupsNoId = peerGroupsToSave.groupsNoId.map((g) => ({
name: g,
peers: [peerGroupsToSave.ID]
}))
peers: [peerGroupsToSave.ID],
}));
if (!groupsNoId.length && !groupsToSave.length) {
return
return;
}
const responsesGroup = yield all(groupsToSave.map(g => call(serviceGroup.editGroup, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: g
})
))
const responsesGroup = yield all(
groupsToSave.map((g) =>
call(serviceGroup.editGroup, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: g,
})
)
);
const responsesGroupNoId = yield all(groupsNoId.map(g => call(serviceGroup.createGroup, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: g
})
))
yield put(actions.saveGroups.success({
loading: false,
success: true,
failure: false,
error: null,
data: [...(responsesGroup as ApiResponse<Group>[]).map(r => r.body), ...(responsesGroupNoId as ApiResponse<Group>[]).map(r => r.body)]
} as CreateResponse<Group[] | null>))
const responsesGroupNoId = yield all(
groupsNoId.map((g) =>
call(serviceGroup.createGroup, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: g,
})
)
);
yield put(groupActions.getGroups.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
yield put(actions.getPeers.request({ getAccessTokenSilently: action.payload.getAccessTokenSilently, payload: null }));
yield put(
actions.saveGroups.success({
loading: false,
success: true,
failure: false,
error: null,
data: [
...(responsesGroup as ApiResponse<Group>[]).map((r) => r.body),
...(responsesGroupNoId as ApiResponse<Group>[]).map((r) => r.body),
],
} as CreateResponse<Group[] | null>)
);
yield put(
groupActions.getGroups.request({
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: null,
})
);
yield put(
actions.getPeers.request({
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: null,
})
);
} catch (err) {
console.log(err)
yield put(actions.saveGroups.failure({
loading: false,
success: false,
failure: true,
error: err as ApiError,
data: null
} as ChangeResponse<Group[] | null>));
console.log(err);
yield put(
actions.saveGroups.failure({
loading: false,
success: false,
failure: true,
error: err as ApiError,
data: null,
} as ChangeResponse<Group[] | null>)
);
}
}
export function* updatePeer(action: ReturnType<typeof actions.updatePeer.request>): Generator {
export function* updatePeer(
action: ReturnType<typeof actions.updatePeer.request>
): Generator {
try {
yield put(actions.setUpdatedPeer({
loading: true,
success: false,
failure: false,
error: null,
data: null
}))
yield put(
actions.setUpdatedPeer({
loading: true,
success: false,
failure: false,
error: null,
data: null,
})
);
const peer = action.payload.payload
const peerId = peer.id
const peer = action.payload.payload;
const peerId = peer.id;
const payloadToSave = {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: peer
}
payload: peer,
};
const effect = yield call(service.updatePeer, payloadToSave)
const effect = yield call(service.updatePeer, payloadToSave);
const response = effect as ApiResponse<Peer>;
yield put(actions.updatePeer.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as ChangeResponse<Peer | null>));
const peers = (yield select(state => state.peer.data)) as Peer[]
yield put(actions.getPeers.success(peers.filter((p:Peer) => p.id !== peerId).concat(response.body)))
yield put(
actions.updatePeer.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body,
} as ChangeResponse<Peer | null>)
);
const peers = (yield select((state) => state.peer.data)) as Peer[];
yield put(
actions.getPeers.success(
peers.filter((p: Peer) => p.id !== peerId).concat(response.body)
)
);
} catch (err) {
console.log(err)
yield put(actions.updatePeer.failure({
loading: false,
success: false,
failure: true,
error: err as ApiError,
data: null
} as ChangeResponse<Peer | null>));
console.log(err);
yield put(
actions.updatePeer.failure({
loading: false,
success: false,
failure: true,
error: err as ApiError,
data: null,
} as ChangeResponse<Peer | null>)
);
}
}
@@ -201,7 +271,6 @@ export default function* sagas(): Generator {
takeLatest(actions.getPeers.request, getPeers),
takeLatest(actions.deletedPeer.request, deletePeer),
takeLatest(actions.saveGroups.request, saveGroups),
takeLatest(actions.updatePeer.request, updatePeer)
takeLatest(actions.updatePeer.request, updatePeer),
]);
}
+81 -72
View File
@@ -434,6 +434,7 @@ export const Peers = () => {
};
const setUpdateGroupsVisible = (peerToAction: Peer, status: boolean) => {
if (!isAdmin) return;
if (status) {
dispatch(peerActions.setPeer({ ...peerToAction }));
dispatch(peerActions.setUpdateGroupsVisible(true));
@@ -502,10 +503,7 @@ export const Peers = () => {
open={groupPopupVisible === peerToAction.key}
title={null}
>
<Button
type="link"
onClick={() => setGroupVisible(peerToAction, true)}
>
<Button type="link" onClick={() => setGroupVisible(peerToAction, true)}>
{label}
</Button>
</Popover>
@@ -592,6 +590,7 @@ export const Peers = () => {
type="text"
style={{ height: "auto", whiteSpace: "normal", textAlign: "left" }}
onClick={() => setUpdateGroupsVisible(peer, true)}
className={!isAdmin ? "nohover" : ""}
>
<span style={{ textAlign: "left" }}>
<Row>
@@ -607,6 +606,7 @@ export const Peers = () => {
<div>
<Button
type="text"
className={!isAdmin ? "nohover" : ""}
style={{ height: "auto", textAlign: "left" }}
onClick={() => setUpdateGroupsVisible(peer, true)}
>
@@ -633,16 +633,16 @@ export const Peers = () => {
<Container style={{ paddingTop: "40px" }}>
<Row>
<Col span={24}>
<Title className="page-heading">Peers</Title>
<Title className="page-heading">{isAdmin ? "Peers" : "My peers"}</Title>
{peers.length ? (
<Paragraph style={{ marginTop: "5px" }}>
A list of all machines and devices connected to your private
network. Use this view to manage peers
{isAdmin ? "A list of all machines and devices connected to your private network. Use this view to manage peers" :
"A list of all your machines and devices that you connected to NetBird."}
</Paragraph>
) : (
<Paragraph style={{ marginTop: "5px" }} type={"secondary"}>
A list of all machines and devices connected to your private
network. Use this view to manage peers
{isAdmin ? "A list of all machines and devices connected to your private network. Use this view to manage peers" :
"A list of all your machines and devices that you connected to NetBird."}
</Paragraph>
)}
@@ -690,22 +690,28 @@ export const Peers = () => {
/>
</Space>
<Select
mode="tags"
placeholder="Filter by groups"
tagRender={blueTagRender}
// dropdownRender={dropDownRender}
optionFilterProp="serchValue"
className="groupsSelect"
onChange={handleGroupChange}
value={groupsFilterArray}
>
{tagGroups.map((m, index) => (
<Option key={index} value={m.id} serchValue={m.name}>
{optionRender(m.name, m.id)}
</Option>
))}
</Select>
{isAdmin && (
<Select
mode="tags"
placeholder="Filter by groups"
tagRender={blueTagRender}
// dropdownRender={dropDownRender}
optionFilterProp="serchValue"
className="groupsSelect"
onChange={handleGroupChange}
value={groupsFilterArray}
>
{tagGroups.map((m, index) => (
<Option
key={index}
value={m.id}
serchValue={m.name}
>
{optionRender(m.name, m.id)}
</Option>
))}
</Select>
)}
</Col>
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
<Row justify="end">
@@ -757,7 +763,7 @@ export const Peers = () => {
Get started by adding one to your network.
</Paragraph>
<Button
data-testid="add-new-peer-button"
data-testid="add-new-peer-button"
size={"middle"}
type="primary"
onClick={() => setAddPeerModalOpen(true)}
@@ -822,53 +828,56 @@ export const Peers = () => {
return renderAddress(record);
}}
/>
<Column
title="Groups"
dataIndex="groupsCount"
align="center"
render={(text, record: PeerDataTable, index) => {
return renderPopoverGroups(
text,
record.groups,
record
);
}}
/>
<Column
title="SSH Server"
dataIndex="ssh_enabled"
align="center"
render={(e, record: PeerDataTable, index) => {
let isWindows = record.os
.toLocaleLowerCase()
.startsWith("windows");
let toggle = (
<Switch
size={"small"}
checked={e}
disabled={isWindows}
onClick={(checked: boolean) => {
if (checked) {
showConfirmEnableSSH(record);
} else {
handleSwitchSSH(record, checked);
}
}}
/>
);
if (isWindows) {
return (
<Tooltip title="SSH server feature is not yet supported on Windows">
{toggle}
</Tooltip>
);
} else {
return toggle;
}
}}
/>
{isAdmin && (
<>
<Column
title="Groups"
dataIndex="groupsCount"
align="center"
render={(text, record: PeerDataTable, index) => {
return renderPopoverGroups(
text,
record.groups,
record
);
}}
/>
<Column
title="SSH Server"
dataIndex="ssh_enabled"
align="center"
render={(e, record: PeerDataTable, index) => {
let isWindows = record.os
.toLocaleLowerCase()
.startsWith("windows");
let toggle = (
<Switch
size={"small"}
checked={e}
disabled={isWindows}
onClick={(checked: boolean) => {
if (checked) {
showConfirmEnableSSH(record);
} else {
handleSwitchSSH(record, checked);
}
}}
/>
);
if (isWindows) {
return (
<Tooltip title="SSH server feature is not yet supported on Windows">
{toggle}
</Tooltip>
);
} else {
return toggle;
}
}}
/>
</>
)}
<Column
title="LastSeen"
dataIndex="last_seen"