Skip to content

Commit 698b654

Browse files
author
dushimsam
committed
feat(UI): added manage group-users page
Signed-off-by: dushimsam <[email protected]>
1 parent 615a373 commit 698b654

File tree

11 files changed

+529
-3
lines changed

11 files changed

+529
-3
lines changed

src/Routes.jsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ const DeleteGroup = React.lazy(() => import("pages/Admin/Group/Delete"));
9494
const DeleteUser = React.lazy(() => import("pages/Admin/Users/Delete"));
9595
const AddUser = React.lazy(() => import("pages/Admin/Users/Add"));
9696
const EditUser = React.lazy(() => import("pages/Admin/Users/Edit"));
97+
const ManageGroup = React.lazy(() => import("pages/Admin/Group/Manage"));
9798
const AddLicense = React.lazy(() => import("pages/Admin/License/Create"));
9899
const SelectLicense = React.lazy(() =>
99100
import("pages/Admin/License/SelectLicense")
@@ -292,6 +293,11 @@ const Routes = () => {
292293
path={routes.admin.group.delete}
293294
component={DeleteGroup}
294295
/>
296+
<AdminLayout
297+
exact
298+
path={routes.admin.group.manageGroup}
299+
component={ManageGroup}
300+
/>
295301
<AdminLayout
296302
exact
297303
path={routes.admin.license.create}

src/api/groups.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,42 @@ export const deleteGroupApi = (id) => {
7676
addGroupName: false,
7777
});
7878
};
79+
80+
// Get all group members
81+
export const getAllGroupMembersApi = (groupId) => {
82+
const url = endpoints.admin.groups.getAllGroupMembers(groupId);
83+
return sendRequest({
84+
url,
85+
method: "GET",
86+
headers: {
87+
Authorization: getToken(),
88+
},
89+
});
90+
};
91+
92+
// Remove Group Member
93+
export const removeGroupMemberApi = (groupId, userId) => {
94+
const url = endpoints.admin.groups.removeGroupMember(groupId, userId);
95+
return sendRequest({
96+
url,
97+
method: "DELETE",
98+
headers: {
99+
Authorization: getToken(),
100+
},
101+
});
102+
};
103+
104+
// Change user permission
105+
export const changeUserPermissionApi = (groupId, userId, permission) => {
106+
const url = endpoints.admin.groups.changeUserPermission(groupId, userId);
107+
return sendRequest({
108+
url,
109+
method: "PUT",
110+
headers: {
111+
Authorization: getToken(),
112+
},
113+
body: {
114+
perm: permission,
115+
},
116+
});
117+
};

src/api/groups.test.js

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@
1515

1616
import sendRequest from "api/sendRequest";
1717
import endpoints from "constants/endpoints";
18-
import { createGroupApi, getAllGroupsApi } from "api/groups";
18+
import {
19+
changeUserPermissionApi,
20+
createGroupApi,
21+
getAllGroupMembersApi,
22+
getAllGroupsApi,
23+
removeGroupMemberApi,
24+
} from "api/groups";
1925
import { getToken } from "shared/authHelper";
2026

2127
jest.mock("api/sendRequest");
@@ -56,4 +62,63 @@ describe("groups", () => {
5662
})
5763
);
5864
});
65+
66+
test("removeGroupMemberApi", () => {
67+
const groupId = 2;
68+
const userId = 1;
69+
const url = endpoints.admin.groups.removeGroupMember(groupId, userId);
70+
sendRequest.mockImplementation(() => true);
71+
72+
expect(removeGroupMemberApi(groupId, userId)).toBe(sendRequest({}));
73+
expect(sendRequest).toHaveBeenCalledWith(
74+
expect.objectContaining({
75+
url,
76+
method: "DELETE",
77+
headers: {
78+
Authorization: getToken(),
79+
},
80+
})
81+
);
82+
});
83+
84+
test("getAllGroupMembersApi", () => {
85+
const groupId = 1;
86+
const url = endpoints.admin.groups.getAllGroupMembers(groupId);
87+
sendRequest.mockImplementation(() => true);
88+
89+
expect(getAllGroupMembersApi(groupId)).toBe(sendRequest({}));
90+
expect(sendRequest).toHaveBeenCalledWith(
91+
expect.objectContaining({
92+
url,
93+
method: "GET",
94+
headers: {
95+
Authorization: getToken(),
96+
},
97+
})
98+
);
99+
});
100+
101+
test("changeUserPermissionApi", () => {
102+
const groupId = 1;
103+
const userId = 1;
104+
const permission = 2;
105+
const url = endpoints.admin.groups.changeUserPermission(groupId, userId);
106+
sendRequest.mockImplementation(() => true);
107+
108+
expect(changeUserPermissionApi(groupId, userId, permission)).toBe(
109+
sendRequest({})
110+
);
111+
expect(sendRequest).toHaveBeenCalledWith(
112+
expect.objectContaining({
113+
url,
114+
method: "PUT",
115+
headers: {
116+
Authorization: getToken(),
117+
},
118+
body: {
119+
perm: permission,
120+
},
121+
})
122+
);
123+
});
59124
});
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*
2+
Copyright (C) 2022 Samuel Dushimimana ([email protected])
3+
4+
SPDX-License-Identifier: GPL-2.0
5+
6+
This program is free software; you can redistribute it and/or
7+
modify it under the terms of the GNU General Public License
8+
version 2 as published by the Free Software Foundation.
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License along
15+
with this program; if not, write to the Free Software Foundation, Inc.,
16+
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17+
*/
18+
19+
import React, { useEffect, useState } from "react";
20+
21+
import { InputContainer } from "components/Widgets";
22+
23+
// Required functions for calling APIs
24+
import { changeUserPermission, removeGroupMember } from "services/groups";
25+
26+
import PropTypes from "prop-types";
27+
28+
// Required constants
29+
import { userPermissions } from "constants/constants";
30+
31+
const ChangePermissionContainer = ({
32+
groupMembers,
33+
noneGroupMembers,
34+
setShowMessage,
35+
setMessage,
36+
currGroup,
37+
handleFetchGroupMembers,
38+
}) => {
39+
const [currUser, setCurrentUser] = useState(null);
40+
const [currNonMember, setCurrentNonMember] = useState(null);
41+
42+
useEffect(() => {
43+
if (groupMembers.length > 0) {
44+
setCurrentUser({
45+
user: groupMembers[0].id,
46+
perm: groupMembers[0].group_perm,
47+
});
48+
}
49+
}, [groupMembers]);
50+
51+
useEffect(() => {
52+
if (noneGroupMembers.length > 0) {
53+
setCurrentNonMember({
54+
user: noneGroupMembers[0].id,
55+
perm: -1,
56+
});
57+
}
58+
}, [noneGroupMembers]);
59+
60+
const handleChangeCurrUser = async (newUser, isMember = true) => {
61+
if (isMember) {
62+
let perm;
63+
groupMembers.forEach((item) => {
64+
if (item.id === parseInt(newUser, 10)) {
65+
perm = item.group_perm;
66+
}
67+
});
68+
setCurrentUser({ user: parseInt(newUser, 10), perm });
69+
} else {
70+
setCurrentNonMember({ user: parseInt(newUser, 10), perm: -1 });
71+
}
72+
};
73+
74+
const handleSetNewPermission = async (newPerm, isMember = true) => {
75+
try {
76+
let res;
77+
78+
if (parseInt(newPerm, 10) === -1) {
79+
res = await removeGroupMember(currGroup, currUser.user);
80+
} else {
81+
res = await changeUserPermission(
82+
currGroup,
83+
isMember ? currUser.user : currNonMember.user,
84+
parseInt(newPerm, 10)
85+
);
86+
}
87+
88+
setShowMessage(true);
89+
setMessage({
90+
type: "success",
91+
text: res.message,
92+
});
93+
94+
handleFetchGroupMembers(currGroup);
95+
} catch (e) {
96+
setMessage({
97+
type: "danger",
98+
text: e.message,
99+
});
100+
} finally {
101+
setTimeout(() => {
102+
setShowMessage(false);
103+
}, [3000]);
104+
}
105+
};
106+
return (
107+
<>
108+
<tr>
109+
<td>
110+
<InputContainer
111+
type="select"
112+
name="name"
113+
options={groupMembers}
114+
id="select-user-tag"
115+
value={currUser?.user}
116+
property="name"
117+
onChange={(e) => handleChangeCurrUser(e.target.value)}
118+
/>
119+
</td>
120+
<td>
121+
<InputContainer
122+
type="select"
123+
name="name"
124+
options={userPermissions}
125+
id="select-tag"
126+
value={currUser?.perm}
127+
property="name"
128+
onChange={(e) => handleSetNewPermission(e.target.value)}
129+
/>
130+
</td>
131+
</tr>
132+
133+
{noneGroupMembers.length > 0 ? (
134+
<tr>
135+
<td>
136+
<InputContainer
137+
type="select"
138+
name="name"
139+
options={noneGroupMembers}
140+
id="select-user-tag"
141+
value={currNonMember?.user}
142+
property="name"
143+
onChange={(e) => handleChangeCurrUser(e.target.value, false)}
144+
/>
145+
</td>
146+
<td>
147+
<InputContainer
148+
type="select"
149+
name="name"
150+
options={userPermissions}
151+
id="select-tag"
152+
value={-1}
153+
property="name"
154+
onChange={(e) => handleSetNewPermission(e.target.value, false)}
155+
/>
156+
</td>
157+
</tr>
158+
) : (
159+
<></>
160+
)}
161+
</>
162+
);
163+
};
164+
165+
ChangePermissionContainer.propTypes = {
166+
groupMembers: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.any)).isRequired,
167+
noneGroupMembers: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.any))
168+
.isRequired,
169+
setMessage: PropTypes.func,
170+
currGroup: PropTypes.number,
171+
handleFetchGroupMembers: PropTypes.func,
172+
setShowMessage: PropTypes.func,
173+
};
174+
175+
export default ChangePermissionContainer;

src/components/Header/index.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,9 +258,9 @@ const Header = () => {
258258
</NavDropdown.Item>
259259
<NavDropdown.Item
260260
as={Link}
261-
to={routes.admin.group.delete}
261+
to={routes.admin.group.manageGroup}
262262
>
263-
Delete Group
263+
Manage Group Users
264264
</NavDropdown.Item>
265265
</div>
266266
</DropdownButton>

src/components/Widgets/Input/index.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const InputContainer = ({
2626
id,
2727
className,
2828
onChange,
29+
defaultValue = null,
2930
children,
3031
checked = false,
3132
placeholder = null,
@@ -70,6 +71,7 @@ const InputContainer = ({
7071
className ? `mr-2 form-control ${className}` : `mr-2 form-control`
7172
}
7273
value={value}
74+
defaultValue={defaultValue}
7375
onChange={onChange}
7476
multiple={multiple && multiple}
7577
size={multiple ? "15" : ""}
@@ -125,6 +127,7 @@ InputContainer.propTypes = {
125127
onChange: PropTypes.func,
126128
checked: PropTypes.bool,
127129
disabled: PropTypes.bool,
130+
defaultValue: PropTypes.string,
128131
children: PropTypes.node,
129132
options: PropTypes.arrayOf(
130133
PropTypes.shape({

src/constants/constants.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,3 +356,23 @@ export const agents = {
356356
reso: "REUSE.Software Analysis (forces *Ojo License Analysis*)",
357357
heritage: "Software Heritage Analysis",
358358
};
359+
360+
// eslint-disable-next-line camelcase
361+
export const userPermissions = [
362+
{
363+
id: -1,
364+
name: "None",
365+
},
366+
{
367+
id: 0,
368+
name: "User",
369+
},
370+
{
371+
id: 1,
372+
name: "Admin",
373+
},
374+
{
375+
id: 2,
376+
name: "Advisor",
377+
},
378+
];

src/constants/endpoints.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ const endpoints = {
7878
getAll: () => `${apiUrl}/groups`,
7979
getAllDeletable: () => `${apiUrl}/groups/deletable`,
8080
delete: (groupId) => `${apiUrl}/groups/${groupId}`,
81+
getAllGroupMembers: (groupId) => `${apiUrl}/groups/${groupId}/members`,
82+
changeUserPermission: (groupId, userId) =>
83+
`${apiUrl}/groups/${groupId}/user/${userId}`,
84+
removeGroupMember: (groupId, userId) =>
85+
`${apiUrl}/groups/${groupId}/user/${userId}`,
8186
},
8287
},
8388
license: {

src/constants/routes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const routes = {
6767
group: {
6868
create: "/admin/group/create",
6969
delete: "/admin/group/delete",
70+
manageGroup: "/admin/group/manage",
7071
},
7172
users: {
7273
add: "/admin/users/add",

0 commit comments

Comments
 (0)