Compare commits

..
8 Commits
Author SHA1 Message Date
Misha Bragin 8e8484cd45 Fix packages (#276) 2023-09-27 12:25:55 +02:00
Sarooj bukhari 6c87f53195 Update the /Activity view to use email addresses from the /api/events response (#273)
uses the events api response information to map user name and email
2023-09-23 10:45:27 +02:00
pascal-fischerandMaycon Santos 9bbbff7dc0 Fix last login for regular entries in users overview (#264)
* add last login to user overview for regular entries without tag

* add backward compatability to properly show with older version of management

* Merge main and update delete user popup messages

---------

Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
2023-09-23 10:40:22 +02:00
Sarooj bukhari 04a20fa31f add new fields on setting pag (#258) 2023-09-22 16:32:40 +02:00
Misha Bragin 3797db93f0 Hide network routes card for non-linux peers (#269) 2023-09-20 10:58:26 +02:00
Sarooj bukhari 2e81765e85 add delete user functionality (#272) 2023-09-19 15:13:31 +02:00
Yoann N cb9f76c0fc typo in ac creation (#266) 2023-09-18 23:22:52 +02:00
Sarooj bukhariandMaycon Santos 54accb665c setup ui (#268)
* setup ui

* capital letter

---------

Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
2023-09-07 20:26:34 +02:00
15 changed files with 1289 additions and 925 deletions

No files matched your search

-11
View File
@@ -1,11 +0,0 @@
{
"auth0Auth": "true",
"authAuthority": "https://netbird-localdev.eu.auth0.com",
"authClientId": "kBRMAOqIZ7hvpVCaypQLCJvTzkYYIXVt",
"authScopesSupported": "openid profile email api offline_access email_verified",
"authAudience": "http://localhost:3000/",
"apiOrigin": "http://0.0.0.0",
"grpcApiOrigin": "http://0.0.0.0:80",
"latestVersion": "v0.6.3"
}
+1 -1
View File
@@ -284,7 +284,7 @@ const AccessControlNew = () => {
<Divider style={{ margin: "8px 0" }} />
<Row style={{ padding: "0 8px 4px" }}>
<Col flex="auto">
<Text type={"secondary"}>Add new ports by pressing "Enter"</Text>
<Text type={"secondary"}>Add new groups by pressing "Enter"</Text>
</Col>
<Col flex="none">
<svg
+9 -1
View File
@@ -179,6 +179,13 @@ const PeerUpdate = (props: any) => {
useEffect(() => {}, [users]);
const routeAddAllowed = (os: string): boolean => {
return os !== ""
&& !os.toLowerCase().startsWith("darwin")
&& !os.toLowerCase().startsWith("windows")
&& !os.toLowerCase().startsWith("android")
}
const toggleEditName = (status: boolean, value?: string) => {
setEditName(status);
@@ -965,6 +972,7 @@ const PeerUpdate = (props: any) => {
{/* --- */}
{!isGroupUpdateView && (
<>
{routeAddAllowed(peer.os) &&
<Card
bordered={true}
// loading={loading}Ć’
@@ -1090,7 +1098,7 @@ const PeerUpdate = (props: any) => {
</Space>
)}
</div>
</Card>
</Card>}
<Card bordered={true} style={{ marginBottom: "50px" }}>
<Col span={24}>
+101 -98
View File
@@ -13,6 +13,7 @@ import {
Tag,
Typography,
Card,
Tooltip,
} from "antd";
import { RootState } from "typesafe-actions";
import {
@@ -54,7 +55,7 @@ const SetupKeyNew = (props: any) => {
const savedSetupKey = useSelector(
(state: RootState) => state.setupKey.savedSetupKey
);
const groups = useSelector((state: RootState) => state.group.data);
const [form] = Form.useForm();
@@ -91,14 +92,13 @@ const SetupKeyNew = (props: any) => {
.filter((g) => allGroups.get(g))
.map((g) => allGroups.get(g)!.name);
}
const fSetupKey = {
...setupKey,
autoGroupNames: setupKey.auto_groups || [],
exp: moment(setupKey.expires),
last: moment(setupKey.last_used),
} as FormSetupKey;
form.setFieldsValue(fSetupKey);
form.setFieldsValue(fSetupKey);
setFormSetupKey(fSetupKey);
}, [setupKey]);
@@ -126,8 +126,7 @@ const SetupKeyNew = (props: any) => {
await form.validateFields();
} catch (e) {
const errorFields = (e as any).errorFields;
return console.log("errorInfo", errorFields);
}
}
const setupKeyToSave = createSetupKeyToSave();
dispatch(
@@ -258,8 +257,7 @@ const SetupKeyNew = (props: any) => {
dispatch(personalAccessTokenActions.resetPersonalAccessTokens(null));
setVisibleNewSetupKey(false);
};
return (
return (
<>
{!isGroupUpdateView && (
<Breadcrumb
@@ -303,13 +301,9 @@ const SetupKeyNew = (props: any) => {
{!isGroupUpdateView && (
<Row style={{ marginTop: "10px" }}>
<Col
xs={24}
sm={24}
md={11}
lg={11}
xl={11}
xxl={11}
span={11}
md={8}
lg={8}
style={{
paddingRight: "70px",
}}
@@ -343,7 +337,14 @@ const SetupKeyNew = (props: any) => {
/>
</Col>
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
<Col
sm={24}
md={8}
lg={6}
style={{
paddingRight: "70px",
}}
>
<Paragraph
style={{
whiteSpace: "pre-line",
@@ -358,8 +359,48 @@ const SetupKeyNew = (props: any) => {
fontWeight: "500",
}}
></Paragraph>
{formSetupKey.type === "one-off" ? "One-off" : "Reusable"},
available uses
Type{" "}
{formSetupKey.ephemeral ? (
<Tooltip title="Peers that are offline for over 10 minutes will be removed automatically">
<Tag>
<Text type="secondary" style={{ fontSize: 10 }}>
Ephemeral
</Text>
</Tag>
</Tooltip>
) : (
" "
)}
</Paragraph>
<Col>
<Input
disabled
value={
formSetupKey.type === "one-off" ? "One-off" : "Reusable"
}
suffix={<LockOutlined style={{ color: "#BFBFBF" }} />}
style={{ marginTop: "8px" }}
/>
</Col>
</Col>
<Col sm={24} md={8} lg={3}>
<Paragraph
style={{
whiteSpace: "pre-line",
margin: 0,
fontWeight: "500",
}}
>
<Paragraph
style={{
whiteSpace: "pre-line",
margin: 0,
fontWeight: "500",
}}
></Paragraph>
{/* {formSetupKey.type === "one-off" ? "One-off" : "Reusable"}, */}
Available uses
</Paragraph>
<Col>
<Input
@@ -378,44 +419,6 @@ const SetupKeyNew = (props: any) => {
</Row>
)}
<Row style={{ marginTop: `${isGroupUpdateView ? "0" : "39px"}` }}>
<Col
xs={24}
sm={24}
md={11}
lg={11}
xl={11}
xxl={11}
span={11}
style={{
paddingRight: "70px",
}}
>
<Paragraph
style={{
whiteSpace: "pre-line",
margin: 0,
fontWeight: "500",
}}
>
<Paragraph
style={{
whiteSpace: "pre-line",
margin: 0,
fontWeight: "500",
}}
></Paragraph>
Ephemeral peers enabled?
</Paragraph>
<Col>
<Input
disabled
value={formSetupKey.ephemeral? "yes" : "no"}
suffix={<LockOutlined style={{ color: "#BFBFBF" }} />}
style={{ marginTop: "8px" }}
/>
</Col>
</Col>
{!isGroupUpdateView && (
<Col xs={24} sm={24} md={5} lg={5} xl={5} xxl={5} span={5}>
<Paragraph style={{ margin: 0, fontWeight: "500" }}>
@@ -434,54 +437,54 @@ const SetupKeyNew = (props: any) => {
</Col>
)}
</Row>
<Row style={{ marginTop: `${isGroupUpdateView ? "0" : "39px"}` }}>
<Col
xs={24}
sm={24}
md={!isGroupUpdateView ? 11 : 24}
lg={!isGroupUpdateView ? 11 : 24}
xl={!isGroupUpdateView ? 11 : 24}
xxl={!isGroupUpdateView ? 11 : 24}
span={!isGroupUpdateView ? 11 : 24}
style={{
paddingRight: `${!isGroupUpdateView ? "70px" : "0"}`,
}}
<Row style={{ marginTop: `${isGroupUpdateView ? "0" : "39px"}` }}>
<Col
xs={24}
sm={24}
md={!isGroupUpdateView ? 11 : 24}
lg={!isGroupUpdateView ? 11 : 24}
xl={!isGroupUpdateView ? 11 : 24}
xxl={!isGroupUpdateView ? 11 : 24}
span={!isGroupUpdateView ? 11 : 24}
style={{
paddingRight: `${!isGroupUpdateView ? "70px" : "0"}`,
}}
>
<Paragraph
style={{
whiteSpace: "pre-line",
margin: 0,
fontWeight: "500",
}}
>
<Paragraph
style={{
whiteSpace: "pre-line",
margin: 0,
fontWeight: "500",
}}
>
Auto-assigned groups
</Paragraph>
Auto-assigned groups
</Paragraph>
<Col span={24}>
<Form.Item
style={{ marginTop: "8px", marginBottom: 0 }}
name="autoGroupNames"
rules={[{ validator: selectValidator }]}
<Col span={24}>
<Form.Item
style={{ marginTop: "8px", marginBottom: 0 }}
name="autoGroupNames"
rules={[{ validator: selectValidator }]}
>
<Select
mode="tags"
style={{ width: "100%" }}
placeholder="Associate groups with the key"
tagRender={blueTagRender}
dropdownRender={dropDownRender}
// enabled only when we have a new key !setupkey.id or when the key is valid
disabled={!(!setupKey.id || setupKey.valid)}
optionFilterProp="searchValue"
>
<Select
mode="tags"
style={{ width: "100%" }}
placeholder="Associate groups with the key"
tagRender={blueTagRender}
dropdownRender={dropDownRender}
// enabled only when we have a new key !setupkey.id or when the key is valid
disabled={!(!setupKey.id || setupKey.valid)}
optionFilterProp="searchValue"
>
{tagGroups.map((m, index) => (
<Option key={index} value={m.id} serchValue={m.name}>
{optionRender(m.name, m.id)}
</Option>
))}
</Select>
</Form.Item>
</Col>
{tagGroups.map((m, index) => (
<Option key={index} value={m.id} serchValue={m.name}>
{optionRender(m.name, m.id)}
</Option>
))}
</Select>
</Form.Item>
</Col>
</Col>
</Row>
</Form>
</div>
+2 -1
View File
@@ -505,7 +505,8 @@ const SetupKeyNew = () => {
type={"secondary"}
style={{ whiteSpace: "pre-line", margin: 0 }}
>
Use this type of key to add peers that will be auto removed after being inactive
Peers that are offline for over 10 minutes will be removed
automatically
</Paragraph>
</Col>
<Col span={6}>
@@ -17,14 +17,16 @@ export const UbuntuTab = () => {
key: 1,
title: "Add repository",
commands: [
`sudo apt-get update`,
`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`,
`curl -sSL https://pkgs.netbird.io/debian/public.key | sudo gpg --dearmor --output /usr/share/keyrings/netbird-archive-keyring.gpg`,
`echo 'deb [signed-by=/usr/share/keyrings/netbird-archive-keyring.gpg] https://pkgs.netbird.io/debian stable main' | sudo tee /etc/apt/sources.list.d/netbird.list`,
].join("\n"),
commandsForCopy: [
`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`,
`sudo apt-get update`,
`sudo apt-get install ca-certificates curl gnupg -y`,
`curl -sSL https://pkgs.netbird.io/debian/public.key | sudo gpg --dearmor --output /usr/share/keyrings/netbird-archive-keyring.gpg`,
`echo 'deb [signed-by=/usr/share/keyrings/netbird-archive-keyring.gpg] https://pkgs.netbird.io/debian stable main' | sudo tee /etc/apt/sources.list.d/netbird.list`,
].join("\n"),
copied: false,
showCopyButton: false,
+9
View File
@@ -519,4 +519,13 @@ ul.ant-list-items {
.nohover {
background: transparent!important;
cursor: text;
}
.emp-wrapper {
display: flex;
align-items: center;
gap: 8px;
}
.emp-wrapper p {
margin: 0;
}
+13 -4
View File
@@ -1,11 +1,20 @@
import {ExpiresInValue} from "../../views/ExpiresInInput";
export interface Account {
id: string;
settings: { peer_login_expiration_enabled: boolean, peer_login_expiration: number}
id: string;
settings: {
peer_login_expiration_enabled: boolean;
peer_login_expiration: number;
jwt_groups_enabled: boolean;
groups_propagation_enabled: boolean;
jwt_groups_claim_name: string;
};
}
export interface FormAccount extends Account {
peer_login_expiration_enabled: boolean
peer_login_expiration_formatted : ExpiresInValue
peer_login_expiration_enabled: boolean;
jwt_groups_enabled: boolean;
groups_propagation_enabled: boolean;
jwt_groups_claim_name: string;
peer_login_expiration_formatted: ExpiresInValue;
}
+9 -7
View File
@@ -1,9 +1,11 @@
export interface Event {
id: string;
timestamp: string
activity: string
activity_code: string
initiator_id: string
target_id: string
meta: { [key: string]: string }
id: string;
timestamp: string;
activity: string;
activity_code: string;
initiator_id: string;
initiator_email: string;
initiator_name: string;
target_id: string;
meta: { [key: string]: string };
}
+48 -30
View File
@@ -1,54 +1,72 @@
import {ActionType, createAction, createAsyncAction} from 'typesafe-actions';
import {User, UserToSave} from './types';
import {ApiError, CreateResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
import { ActionType, createAction, createAsyncAction } from "typesafe-actions";
import { User, UserToSave } from "./types";
import {
ApiError,
CreateResponse,
DeleteResponse,
RequestPayload,
} from "../../services/api-client/types";
const actions = {
getUsers: createAsyncAction(
'GET_USERS_REQUEST',
'GET_USERS_SUCCESS',
'GET_USERS_FAILURE',
"GET_USERS_REQUEST",
"GET_USERS_SUCCESS",
"GET_USERS_FAILURE"
)<RequestPayload<null>, User[], ApiError>(),
getServiceUsers: createAsyncAction(
'GET_SERVICE_USERS_REQUEST',
'GET_SERVICE_USERS_SUCCESS',
'GET_SERVICE_USERS_FAILURE',
"GET_SERVICE_USERS_REQUEST",
"GET_SERVICE_USERS_SUCCESS",
"GET_SERVICE_USERS_FAILURE"
)<RequestPayload<null>, User[], ApiError>(),
getRegularUsers: createAsyncAction(
'GET_REGULAR_USERS_REQUEST',
'GET_REGULAR_USERS_SUCCESS',
'GET_REGULAR_USERS_FAILURE',
"GET_REGULAR_USERS_REQUEST",
"GET_REGULAR_USERS_SUCCESS",
"GET_REGULAR_USERS_FAILURE"
)<RequestPayload<null>, User[], ApiError>(),
deleteUser: createAsyncAction(
'DELETE_USER_REQUEST',
'DELETE_USER_SUCCESS',
'DELETE_USER_FAILURE',
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
setDeletedUser: createAction('SET_DELETED_USER')<DeleteResponse<string | null>>(),
resetDeletedUser: createAction('RESET_DELETED_USER')<null>(),
"DELETE_USER_REQUEST",
"DELETE_USER_SUCCESS",
"DELETE_USER_FAILURE"
)<
RequestPayload<string>,
DeleteResponse<string | null>,
DeleteResponse<string | null>
>(),
setDeletedUser:
createAction("SET_DELETED_USER")<DeleteResponse<string | null>>(),
resetDeletedUser: createAction("RESET_DELETED_USER")<null>(),
// used to set a user object that was picked in the user table in the UserUpdate drawer (user update window on right-side).
setUser: createAction('SET_USER')<User>(),
setUser: createAction("SET_USER")<User>(),
// used to make the UserUpdate drawer visible in the UI.
setUpdateUserDrawerVisible: createAction('SET_UPDATE_USER_VISIBLE')<boolean>(),
setUpdateUserDrawerVisible: createAction(
"SET_UPDATE_USER_VISIBLE"
)<boolean>(),
// used to make the ViewUserPopup visible in the UI.
setInviteUserPopupVisible: createAction('SET_INVITE_USER_VISIBLE')<boolean>(),
setInviteUserPopupVisible: createAction("SET_INVITE_USER_VISIBLE")<boolean>(),
// used to make the EditUserPopup visible in the UI.
setEditUserPopupVisible: createAction('SET_EDIT_USER_VISIBLE')<boolean>(),
setEditUserPopupVisible: createAction("SET_EDIT_USER_VISIBLE")<boolean>(),
// used to make the AddServiceUserPopup visible in the UI.
setAddServiceUserPopupVisible: createAction('SET_ADD_SERVICE_USER_VISIBLE')<boolean>(),
setAddServiceUserPopupVisible: createAction(
"SET_ADD_SERVICE_USER_VISIBLE"
)<boolean>(),
// used to remember what tab was open on users page
setUserTabOpen: createAction('SET_USER_TAB_OPEN')<string>(),
setUserTabOpen: createAction("SET_USER_TAB_OPEN")<string>(),
saveUser: createAsyncAction(
'SAVE_USER_REQUEST',
'SAVE_USER_SUCCESS',
'SAVE_USER_FAILURE',
)<RequestPayload<UserToSave>, CreateResponse<User | null>, CreateResponse<User | null>>(),
setSavedUser: createAction('SET_SAVED_USER')<CreateResponse<User | null>>(),
resetSavedUser: createAction('RESET_SAVED_USER')<null>(),
"SAVE_USER_REQUEST",
"SAVE_USER_SUCCESS",
"SAVE_USER_FAILURE"
)<
RequestPayload<UserToSave>,
CreateResponse<User | null>,
CreateResponse<User | null>
>(),
setSavedUser: createAction("SET_SAVED_USER")<CreateResponse<User | null>>(),
resetSavedUser: createAction("RESET_SAVED_USER")<null>(),
};
export type ActionTypes = ActionType<typeof actions>;
+162 -138
View File
@@ -1,159 +1,183 @@
import {all, call, put, select, takeLatest} from 'redux-saga/effects';
import {ApiError, ApiResponse, CreateResponse, DeleteResponse} from '../../services/api-client/types';
import {User, UserToSave} from './types'
import service from './service';
import actions from './actions';
import { all, call, put, select, takeLatest } from "redux-saga/effects";
import {
ApiError,
ApiResponse,
CreateResponse,
DeleteResponse,
} from "../../services/api-client/types";
import { User, UserToSave } from "./types";
import service from "./service";
import actions from "./actions";
import serviceGroup from "../group/service";
import {Group} from "../group/types";
import { Group } from "../group/types";
export function* getUsers(action: ReturnType<typeof actions.getUsers.request>): Generator {
try {
const effect = yield call(service.getUsers, action.payload);
const response = effect as ApiResponse<User[]>;
export function* getUsers(
action: ReturnType<typeof actions.getUsers.request>
): Generator {
try {
const effect = yield call(service.getUsers, action.payload);
const response = effect as ApiResponse<User[]>;
yield put(actions.getUsers.success(response.body));
} catch (err) {
yield put(actions.getUsers.failure(err as ApiError));
}
yield put(actions.getUsers.success(response.body));
} catch (err) {
yield put(actions.getUsers.failure(err as ApiError));
}
}
export function* getServiceUsers(action: ReturnType<typeof actions.getServiceUsers.request>): Generator {
try {
action.payload.queryParams = {service_user: true}
const effect = yield call(service.getUsers, action.payload);
const response = effect as ApiResponse<User[]>;
export function* getServiceUsers(
action: ReturnType<typeof actions.getServiceUsers.request>
): Generator {
try {
action.payload.queryParams = { service_user: true };
const effect = yield call(service.getUsers, action.payload);
const response = effect as ApiResponse<User[]>;
yield put(actions.getServiceUsers.success(response.body));
} catch (err) {
yield put(actions.getServiceUsers.failure(err as ApiError));
}
yield put(actions.getServiceUsers.success(response.body));
} catch (err) {
yield put(actions.getServiceUsers.failure(err as ApiError));
}
}
export function* getRegularUsers(action: ReturnType<typeof actions.getRegularUsers.request>): Generator {
try {
action.payload.queryParams = {service_user: false}
const effect = yield call(service.getUsers, action.payload);
const response = effect as ApiResponse<User[]>;
export function* getRegularUsers(
action: ReturnType<typeof actions.getRegularUsers.request>
): Generator {
try {
action.payload.queryParams = { service_user: false };
const effect = yield call(service.getUsers, action.payload);
const response = effect as ApiResponse<User[]>;
yield put(actions.getRegularUsers.success(response.body));
} catch (err) {
yield put(actions.getRegularUsers.failure(err as ApiError));
}
yield put(actions.getRegularUsers.success(response.body));
} catch (err) {
yield put(actions.getRegularUsers.failure(err as ApiError));
}
}
export function* saveUser(action: ReturnType<typeof actions.saveUser.request>): Generator {
try {
yield put(actions.setSavedUser({
loading: true,
success: false,
failure: false,
error: null,
data: null
} as CreateResponse<User | null>))
export function* saveUser(
action: ReturnType<typeof actions.saveUser.request>
): Generator {
try {
yield put(
actions.setSavedUser({
loading: true,
success: false,
failure: false,
error: null,
data: null,
} as CreateResponse<User | null>)
);
const userToSave = action.payload.payload
const userToSave = action.payload.payload;
let groupsToCreate = userToSave.groupsToCreate
if (!groupsToCreate) {
groupsToCreate = []
}
// first, create groups that were newly added by user
const responsesGroup = yield all(groupsToCreate.map(g => call(serviceGroup.createGroup, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: {name: g}
})
))
const resGroups = (responsesGroup as ApiResponse<Group>[]).filter(r => r.statusCode === 200).map(g => (g.body as Group)).map(g => g.id)
const newGroups = [...userToSave.auto_groups, ...resGroups]
let payload = {
name: userToSave.name,
email: userToSave.email,
role: userToSave.role,
auto_groups: newGroups,
is_service_user: userToSave.is_service_user,
is_blocked: userToSave.is_blocked
} as UserToSave
let effect
if (!userToSave.id) {
effect = yield call(service.createUser, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: payload
});
} else {
payload.id = userToSave.id
effect = yield call(service.editUser, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: payload
});
}
const response = effect as ApiResponse<User>;
yield put(actions.saveUser.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as CreateResponse<User | null>));
} catch (err) {
yield put(actions.saveUser.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null
} as CreateResponse<User | null>));
let groupsToCreate = userToSave.groupsToCreate;
if (!groupsToCreate) {
groupsToCreate = [];
}
// first, create groups that were newly added by user
const responsesGroup = yield all(
groupsToCreate.map((g) =>
call(serviceGroup.createGroup, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: { name: g },
})
)
);
const resGroups = (responsesGroup as ApiResponse<Group>[])
.filter((r) => r.statusCode === 200)
.map((g) => g.body as Group)
.map((g) => g.id);
const newGroups = [...userToSave.auto_groups, ...resGroups];
let payload = {
name: userToSave.name,
email: userToSave.email,
role: userToSave.role,
auto_groups: newGroups,
is_service_user: userToSave.is_service_user,
is_blocked: userToSave.is_blocked,
} as UserToSave;
let effect;
if (!userToSave.id) {
effect = yield call(service.createUser, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: payload,
});
} else {
payload.id = userToSave.id;
effect = yield call(service.editUser, {
getAccessTokenSilently: action.payload.getAccessTokenSilently,
payload: payload,
});
}
const response = effect as ApiResponse<User>;
yield put(
actions.saveUser.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body,
} as CreateResponse<User | null>)
);
} catch (err) {
yield put(
actions.saveUser.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null,
} as CreateResponse<User | null>)
);
}
}
export function* deleteUser(action: ReturnType<typeof actions.deleteUser.request>): Generator {
try {
yield call(actions.setDeletedUser,{
loading: true,
success: false,
failure: false,
error: null,
data: null
} as DeleteResponse<string | null>)
export function* deleteUser(
action: ReturnType<typeof actions.deleteUser.request>
): Generator {
try {
yield put(
actions.setDeletedUser({
loading: true,
success: false,
failure: false,
error: null,
data: null,
} as DeleteResponse<string | null>)
);
const effect = yield call(service.deleteUser, action.payload);
const response = effect as ApiResponse<any>;
yield put(actions.deleteUser.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body
} as DeleteResponse<string | null>));
const users = (yield select(state => state.users.data)) as User[]
const regularUsers = (yield select(state => state.users.regularUsers)) as User[]
const serviceUsers = (yield select(state => state.users.serviceUsers)) as User[]
yield put(actions.getUsers.success(users.filter((p:User) => p.id !== action.payload.payload)))
yield put(actions.getRegularUsers.success(regularUsers.filter((p:User) => p.id !== action.payload.payload)))
yield put(actions.getServiceUsers.success(serviceUsers.filter((p:User) => p.id !== action.payload.payload)))
} catch (err) {
yield put(actions.deleteUser.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null
} as DeleteResponse<string | null>));
}
const effect = yield call(service.deleteUser, action.payload);
const response = effect as ApiResponse<any>;
yield put(
actions.deleteUser.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body,
} as DeleteResponse<string | null>)
);
} catch (err) {
yield put(
actions.deleteUser.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null,
} as DeleteResponse<string | null>)
);
}
}
export default function* sagas(): Generator {
yield all([
takeLatest(actions.getUsers.request, getUsers),
takeLatest(actions.getServiceUsers.request, getServiceUsers),
takeLatest(actions.getRegularUsers.request, getRegularUsers),
takeLatest(actions.saveUser.request, saveUser),
takeLatest(actions.deleteUser.request, deleteUser)
]);
yield all([
takeLatest(actions.getUsers.request, getUsers),
takeLatest(actions.getServiceUsers.request, getServiceUsers),
takeLatest(actions.getRegularUsers.request, getRegularUsers),
takeLatest(actions.saveUser.request, saveUser),
takeLatest(actions.deleteUser.request, deleteUser),
]);
}
+42 -51
View File
@@ -168,37 +168,36 @@ export const Activity = () => {
};
const renderInitiator = (event: EventDataTable) => {
let body = <></>;
const user = users?.find((u) => u.id === event.initiator_id);
if (event.initiator_id == "sys") {
body = (
<span
style={{
height: "auto",
whiteSpace: "normal",
textAlign: "left",
}}
>
<Row>
<Text type="secondary">System</Text>
</Row>
</span>
);
<span
style={{
height: "auto",
whiteSpace: "normal",
textAlign: "left",
}}
>
<Row>
<Text type="secondary">System</Text>
</Row>
</span>
);
}
switch (event.activity_code) {
case "peer.login.expire":
body = (
<span
style={{
height: "auto",
whiteSpace: "normal",
textAlign: "left",
}}
>
<Row>
<Text type="secondary">System</Text>
</Row>
</span>
);
<span
style={{
height: "auto",
whiteSpace: "normal",
textAlign: "left",
}}
>
<Row>
<Text type="secondary">System</Text>
</Row>
</span>
);
break;
case "setupkey.peer.add":
const key = setupKeys?.find((k) => k.id === event.initiator_id);
@@ -224,7 +223,7 @@ export const Activity = () => {
}
break;
default:
if (user) {
if (event.initiator_name || event.initiator_email) {
body = (
<span
style={{
@@ -235,16 +234,16 @@ export const Activity = () => {
>
<Row>
{" "}
<Text>{user.name ? user.name : user.id}</Text>{" "}
<Text>
{event.initiator_name
? event.initiator_name
: event.initiator_id}
</Text>{" "}
</Row>
<Row>
{" "}
<Text type="secondary">
{user.email
? user.email
: user.is_service_user
? "Service User"
: "User"}
{event.initiator_email ? event.initiator_email : "User"}
</Text>{" "}
</Row>
</span>
@@ -281,7 +280,6 @@ export const Activity = () => {
) {
return "-";
}
const user = users?.find((u) => u.id === event.target_id);
switch (event.activity_code) {
case "account.create":
case "user.join":
@@ -334,14 +332,10 @@ export const Activity = () => {
case "user.group.add":
case "user.group.delete":
case "user.role.update":
if (user) {
if (event.meta.email || event.meta.username || event.target_id) {
return renderMultiRowSpan(
user.name ? user.name : user.id,
user.email
? user.email
: user.is_service_user
? "Service User"
: "User"
event.meta.username ? event.meta.username : event.target_id,
event.meta.email ? event.meta.email : "User"
);
}
if (event.meta.user_name) {
@@ -365,14 +359,10 @@ export const Activity = () => {
return renderMultiRowSpan("", "System setting");
case "personal.access.token.create":
case "personal.access.token.delete":
if (user) {
if (event.meta.email || event.meta.username || event.target_id) {
return renderMultiRowSpan(
user.name ? user.name : user.id,
user.email
? user.email
: user.is_service_user
? "Service User"
: "User"
event.meta.username ? event.meta.username : event.target_id,
event.meta.email ? event.meta.email : "User"
);
}
if (event.meta.user_name) {
@@ -384,14 +374,15 @@ export const Activity = () => {
return "-";
case "service.user.create":
case "service.user.delete":
return renderMultiRowSpan(event.meta.name, "Service User");
return renderMultiRowSpan(event.meta.username, "Service User");
case "user.invite":
case "user.block":
case "user.delete":
case "user.unblock":
if (user) {
if (event.meta.email || event.meta.username || event.target_id) {
return renderMultiRowSpan(
user.name ? user.name : user.id,
user.email ? user.email : "User"
event.meta.username ? event.meta.username : event.target_id,
event.meta.email ? event.meta.email : "User"
);
}
break;
+137 -25
View File
@@ -31,7 +31,12 @@ 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 {capitalize, isLocalDev, isNetBirdHosted, timeAgo} from "../utils/common";
import {
capitalize,
isLocalDev,
isNetBirdHosted,
timeAgo,
} from "../utils/common";
import { usePageSizeHelpers } from "../utils/pageSize";
import AddServiceUserPopup from "../components/popups/AddServiceUserPopup";
import InviteUserPopup from "../components/popups/InviteUserPopup";
@@ -56,13 +61,14 @@ export const RegularUsers = () => {
const dispatch = useDispatch();
const [isAdmin, setIsAdmin] = useState(false);
const [showGroupModal, setShowGroupModal] = useState(false);
const [showGroupModal, setShowGroupModal] = 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);
const loading = useSelector((state: RootState) => state.user.loading);
const deleteUser = useSelector((state: RootState) => state.user.deletedUser);
const updateUserDrawerVisible = useSelector(
(state: RootState) => state.user.updateUserDrawerVisible
);
@@ -76,6 +82,42 @@ export const RegularUsers = () => {
const [dataTable, setDataTable] = useState([] as UserDataTable[]);
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
const deleteKey = "deleting";
useEffect(() => {
const style = { marginTop: 85 };
if (deleteUser.loading) {
message.loading({ content: "Deleting...", key: deleteKey, style });
} else if (deleteUser.success) {
message.success({
content: "User has been successfully deleted.",
key: deleteKey,
duration: 2,
style,
});
dispatch(
userActions.getRegularUsers.request({
getAccessTokenSilently: getTokenSilently,
payload: null,
})
);
dispatch(
groupActions.getGroups.request({
getAccessTokenSilently: getTokenSilently,
payload: null,
})
);
dispatch(userActions.resetDeletedUser(null));
} else if (deleteUser.error) {
message.error({
content: "Failed to delete user.",
key: deleteKey,
duration: 2,
style,
});
dispatch(userActions.resetDeletedUser(null));
}
}, [deleteUser]);
// setUserAndView makes the UserUpdate drawer visible (right side) and sets the user object
const setUserAndView = (user: User) => {
dispatch(userActions.setUpdateUserDrawerVisible(true));
@@ -92,7 +134,7 @@ export const RegularUsers = () => {
};
const setUserAndViewGroups = (user: User) => {
dispatch(
dispatch(
userActions.setUser({
id: user.id,
email: user.email,
@@ -101,8 +143,8 @@ export const RegularUsers = () => {
name: user.name,
is_current: user.is_current,
} as User)
);
setShowGroupModal(true)
);
setShowGroupModal(true);
};
const transformDataTable = (d: User[]): UserDataTable[] => {
@@ -286,7 +328,7 @@ export const RegularUsers = () => {
dispatch(userActions.setUpdateUserDrawerVisible(false));
dispatch(userActions.setSavedUser({ ...savedUser, success: false }));
dispatch(userActions.resetSavedUser(null));
setShowGroupModal(false)
setShowGroupModal(false);
} else if (savedUser.error) {
let errorMsg = "Failed to update user";
switch (savedUser.error.statusCode) {
@@ -354,7 +396,7 @@ export const RegularUsers = () => {
icon: <ExclamationCircleOutlined />,
title: (
<span className="font-500">
Are you sure you want to block {user.name} ?
Are you sure you want to block {user.name? user.name : user.id}?
</span>
),
width: 600,
@@ -405,6 +447,31 @@ export const RegularUsers = () => {
} as UserToSave;
};
const handleDeleteUser = (user: UserDataTable) => {
confirmModal.confirm({
icon: <ExclamationCircleOutlined />,
title: <span className="font-500">Are you sure you want to delete {user.name? user.name : user.id}?</span>,
width: 500,
content: (
<Space direction="vertical" size="small">
<Paragraph>
Deleting this user will remove their devices and remove dashboard access.</Paragraph>
</Space>
),
onOk() {
dispatch(
userActions.deleteUser.request({
getAccessTokenSilently: getTokenSilently,
payload: user.id,
})
);
},
onCancel() {
// noop
},
});
};
return (
<>
<Container style={{ padding: "0px" }}>
@@ -499,19 +566,19 @@ export const RegularUsers = () => {
defaultSortOrder="ascend"
render={(text, record, index) => {
const btn = (
<Button
type="text"
onClick={() =>
handleEditUser(record as UserDataTable)
}
className="tooltip-label"
>
<Text className="font-500">
{text && text.trim() !== ""
? text
: (record as User).id}
</Text>
</Button>
<Button
type="text"
onClick={() =>
handleEditUser(record as UserDataTable)
}
className="tooltip-label"
>
<Text className="font-500">
{text && text.trim() !== ""
? text
: (record as User).id}
</Text>
</Button>
);
if ((record as User).is_current) {
@@ -521,7 +588,7 @@ export const RegularUsers = () => {
{btn}
<Tag color="blue">me</Tag>
</div>
<Text type={"secondary"} style={{paddingLeft: "15px"}}>Last login: {String(timeAgo((record as User).last_login))}</Text>
{((record as User).last_login && <Text type={"secondary"} style={{paddingLeft: "15px"}}>Last login: {String(timeAgo((record as User).last_login))}</Text>)}
</>
);
}
@@ -533,7 +600,7 @@ export const RegularUsers = () => {
{btn}
<Tag color="gold">invited</Tag>
</div>
<Text type={"secondary"} style={{paddingLeft: "15px", paddingTop: "0px"}}>Last login: {String(timeAgo((record as User).last_login))}</Text>
{((record as User).last_login && <Text type={"secondary"} style={{paddingLeft: "15px", paddingTop: "0px"}}>Last login: {String(timeAgo((record as User).last_login))}</Text>)}
</>
);
}
@@ -545,12 +612,19 @@ export const RegularUsers = () => {
{btn}
<Tag color="red">blocked</Tag>
</div>
<Text type={"secondary"} style={{paddingLeft: "15px"}}>Last login: {String(timeAgo((record as User).last_login))}</Text>
{((record as User).last_login && <Text type={"secondary"} style={{paddingLeft: "15px"}}>Last login: {String(timeAgo((record as User).last_login))}</Text>)}
</>
);
}
return btn;
return (
<>
<div>
{btn}
</div>
{((record as User).last_login && <Text type={"secondary"} style={{paddingLeft: "15px", paddingTop: "0px"}}>Last login: {String(timeAgo((record as User).last_login))}</Text>)}
</>
);
}}
/>
<Column
@@ -619,6 +693,44 @@ export const RegularUsers = () => {
}}
/>
)}
{isAdmin && (
<Column
title="Delete user"
align="center"
width="150px"
// dataIndex="is_blocked"
render={(e, record: UserDataTable, index) => {
let witch = (
<Button
danger={true}
type={"text"}
style={{ marginLeft: "3px", marginRight: "3px" }}
onClick={() => {
let userRecord = record as UserDataTable;
handleDeleteUser(userRecord);
}}
>
Delete
</Button>
);
if (record.is_current) {
return (
<Tooltip title="You can't delete yourself">
<Empty
image={""}
description={""}
style={{ height: "1px", width: "auto" }}
/>
</Tooltip>
);
}
return witch;
}}
/>
)}
</Table>
</Card>
</Space>
+691 -507
View File
File diff suppressed because it is too large. Load diff
+58 -46
View File
@@ -20,6 +20,7 @@ import {
Space,
Table,
Tag,
Tooltip,
Typography,
} from "antd";
import { SetupKey, SetupKeyToSave } from "../store/setup-key/types";
@@ -108,28 +109,27 @@ export const SetupKeys = () => {
);
}, []);
useEffect(() => {
setDataTable(transformDataTable(filterDataTable("")));
}, [textToSearch, optionValidAll]);
useEffect(() => {
if (!loading) {
const quickFilter = getFilterState("setupKeysFilter", "quickFilter");
if (quickFilter) setOptionValidAll(quickFilter);
const quickFilter = getFilterState("setupKeysFilter", "quickFilter");
if (quickFilter) setOptionValidAll(quickFilter);
const searchText = getFilterState("setupKeysFilter", "search");
if (searchText) setTextToSearch(searchText);
const searchText = getFilterState("setupKeysFilter", "search");
if (searchText) setTextToSearch(searchText);
const pageSize = getFilterState("setupKeysFilter", "pageSize");
if (pageSize) onChangePageSize(pageSize, "setupKeysFilter");
const pageSize = getFilterState("setupKeysFilter", "pageSize");
if (pageSize) onChangePageSize(pageSize, "setupKeysFilter");
if (quickFilter || searchText || pageSize) {
setDataTable(transformDataTable(filterDataTable(searchText)));
} else {
setDataTable(transformDataTable(filterDataTable("")));
}
}
if (quickFilter || searchText || pageSize) {
setDataTable(transformDataTable(filterDataTable(searchText)));
} else {
setDataTable(transformDataTable(filterDataTable("")));
}
}
}, [loading]);
const deleteKey = "deleting";
@@ -184,7 +184,7 @@ export const SetupKeys = () => {
);
dispatch(setupKeyActions.resetSavedSetupKey(null));
dispatch(setupKeyActions.setSetupEditKeyVisible(false));
setShowGroupModal(false)
setShowGroupModal(false);
} else if (savedSetupKey.error) {
message.error({
content:
@@ -201,9 +201,9 @@ export const SetupKeys = () => {
}, [savedSetupKey]);
const filterDataTable = (searchText: string): SetupKey[] => {
const t = searchText
? searchText.toLowerCase().trim()
: textToSearch.toLowerCase().trim();
const t = searchText
? searchText.toLowerCase().trim()
: textToSearch.toLowerCase().trim();
let f: SetupKey[] = [...setupKeys];
if (optionValidAll === "valid") {
f = filter(setupKeys, (_f: SetupKey) => _f.valid && !_f.revoked);
@@ -296,6 +296,7 @@ export const SetupKeys = () => {
auto_groups: key?.auto_groups,
last_used: key?.last_used,
usage_limit: key?.usage_limit,
ephemeral: key?.ephemeral,
} as SetupKey)
);
};
@@ -317,7 +318,7 @@ export const SetupKeys = () => {
usage_limit: key?.usage_limit,
} as SetupKey)
);
setShowGroupModal(true)
setShowGroupModal(true);
};
useEffect(() => {
@@ -358,7 +359,9 @@ export const SetupKeys = () => {
let btn = (
<Button
type="link"
onClick={() => setKeyAndViewGroups(setupKeyToAction as SetupKeyDataTable)}
onClick={() =>
setKeyAndViewGroups(setupKeyToAction as SetupKeyDataTable)
}
>
{displayGroups.length}
</Button>
@@ -576,14 +579,6 @@ export const SetupKeys = () => {
(a as any).name.localeCompare((b as any).name)
}
render={(text, record: any, index) => {
let sk = record as SetupKeyDataTable;
let expiry = sk.ephemeral ? (
<Tag>
<Text type="secondary" style={{ fontSize: 10 }}>
ephemeral peers enabled
</Text>
</Tag>
) : null;
return (
<Button
type="text"
@@ -592,25 +587,22 @@ export const SetupKeys = () => {
}
className="tooltip-label"
>
<span style={{ textAlign: "left" }}>
<Row>
{" "}
<Text className="font-500">
<Badge
size={"small"}
status={
record.state === "valid"
? "success"
: "error"
}
text={text}
></Badge>
</Text>
</Row>
<Row >
{expiry}
</Row>
</span>
<span style={{ textAlign: "left" }}>
<Row>
{" "}
<Text className="font-500">
<Badge
size={"small"}
status={
record.state === "valid"
? "success"
: "error"
}
text={text}
></Badge>
</Text>
</Row>
</span>
</Button>
);
}}
@@ -625,6 +617,26 @@ export const SetupKeys = () => {
sorter={(a, b) =>
(a as any).type.localeCompare((b as any).type)
}
render={(text, record, index) => {
let sk = record as SetupKeyDataTable;
let expiry = sk.ephemeral ? (
<Tooltip title="Peers that are offline for over 10 minutes will be removed automatically">
<Tag>
<Text type="secondary" style={{ fontSize: 10 }}>
Ephemeral
</Text>
</Tag>
</Tooltip>
) : null;
return (
<>
<div className="emp-wrapper">
<p>{sk.type}</p>
{expiry}
</div>
</>
);
}}
/>
<Column
title="Key"