diff --git a/backend/app/deps/authorization_deps.py b/backend/app/deps/authorization_deps.py
index 25639e108..22d2caf1f 100644
--- a/backend/app/deps/authorization_deps.py
+++ b/backend/app/deps/authorization_deps.py
@@ -115,7 +115,7 @@ async def get_role_by_group(
if group_out.creator == current_user:
# Creator can do everything
return RoleType.OWNER
- for u in group.users:
+ for u in group_out.users:
if u.user.email == current_user:
if u.editor:
return RoleType.EDITOR
diff --git a/backend/app/routers/groups.py b/backend/app/routers/groups.py
index c9edc92c7..4ac5d9e4b 100644
--- a/backend/app/routers/groups.py
+++ b/backend/app/routers/groups.py
@@ -1,15 +1,15 @@
+import re
from datetime import datetime
-from http.client import HTTPException
+from fastapi import HTTPException, Depends, APIRouter
from bson.objectid import ObjectId
-from fastapi.params import Depends
-from fastapi.routing import APIRouter
from pymongo import DESCENDING
from pymongo.mongo_client import MongoClient
-from typing import List
+from typing import List, Optional
from app import dependencies
from app.deps.authorization_deps import AuthorizationDB, GroupAuthorization
from app.keycloak_auth import get_current_user, get_user
+from app.models.authorization import RoleType
from app.models.groups import GroupOut, GroupIn, GroupDB, GroupBase, Member
from app.models.users import UserOut
@@ -68,6 +68,48 @@ async def get_groups(
return groups
+@router.get("/search/{search_term}", response_model=List[GroupOut])
+async def search_group(
+ search_term: str,
+ user_id=Depends(get_user),
+ db: MongoClient = Depends(dependencies.get_db),
+ skip: int = 0,
+ limit: int = 10,
+):
+ """Search all groups in the db based on text.
+
+ Arguments:
+ text -- any text matching name or description
+ skip -- number of initial records to skip (i.e. for pagination)
+ limit -- restrict number of records to be returned (i.e. for pagination)
+ """
+
+ # Check all connection and abort if any one of them is not available
+ if db is None:
+ raise HTTPException(status_code=503, detail="Service not available")
+ return
+
+ groups = []
+ query_regx = re.compile(search_term, re.IGNORECASE)
+ for doc in (
+ # user has to be the creator or member first; then apply search
+ await db["groups"]
+ .find(
+ {
+ "$and": [
+ {"$or": [{"creator": user_id}, {"users.user.email": user_id}]},
+ {"$or": [{"name": query_regx}, {"description": query_regx}]},
+ ]
+ }
+ )
+ .skip(skip)
+ .limit(limit)
+ .to_list(length=limit)
+ ):
+ groups.append(GroupOut.from_mongo(doc))
+ return groups
+
+
@router.get("/{group_id}", response_model=GroupOut)
async def get_group(
group_id: str,
@@ -113,45 +155,30 @@ async def edit_group(
raise HTTPException(status_code=404, detail=f"Group {group_id} not found")
-@router.delete("/{group_id}")
+@router.delete("/{group_id}", response_model=GroupOut)
async def delete_group(
group_id: str,
db: MongoClient = Depends(dependencies.get_db),
allow: bool = Depends(GroupAuthorization("owner")),
):
- if (group := await db["groups"].find_one({"_id": ObjectId(group_id)})) is not None:
+ if (
+ group_q := await db["groups"].find_one({"_id": ObjectId(group_id)})
+ ) is not None:
await db["groups"].delete_one({"_id": ObjectId(group_id)})
- return {"deleted": group_id}
+ return GroupDB.from_mongo(group_q)
else:
raise HTTPException(status_code=404, detail=f"Dataset {group_id} not found")
-@router.get("/search/{search_term}")
-async def search_group(
- search_term: str, db: MongoClient = Depends(dependencies.get_db)
-):
- # Check all connection and abort if any one of them is not available
- if db is None:
- raise HTTPException(status_code=503, detail="Service not available")
- return
-
- groups = []
- pattern = ".*" + search_term + ".*"
- async for f in db["groups"].find({"name": {"$regex": pattern, "$options": "i"}}):
- groups.append(GroupDB.from_mongo(f))
-
- return groups
-
-
-@router.post("/{group_id}/add/{username}")
+@router.post("/{group_id}/add/{username}", response_model=GroupOut)
async def add_member(
group_id: str,
username: str,
+ role: Optional[str] = None,
db: MongoClient = Depends(dependencies.get_db),
allow: bool = Depends(GroupAuthorization("editor")),
):
"""Add a new user to a group."""
-
if (user_q := await db["users"].find_one({"email": username})) is not None:
new_member = Member(user=UserOut.from_mongo(user_q))
if (
@@ -165,6 +192,12 @@ async def add_member(
break
if not found_already:
# If user is already in the group, skip directly to returning the group
+ # else add role and attach this member
+
+ if role is not None and role == RoleType.EDITOR:
+ new_member.editor = True
+ else:
+ new_member.editor = False
group.users.append(new_member)
await db["groups"].replace_one(
{"_id": ObjectId(group_id)}, group.to_mongo()
@@ -213,3 +246,43 @@ async def remove_member(
return group
raise HTTPException(status_code=404, detail=f"Group {group_id} not found")
+
+
+@router.put("/{group_id}/update/{username}", response_model=GroupOut)
+async def update_member(
+ group_id: str,
+ username: str,
+ role: str,
+ db: MongoClient = Depends(dependencies.get_db),
+ allow: bool = Depends(GroupAuthorization("editor")),
+):
+ """Update user role."""
+ if (user_q := await db["users"].find_one({"email": username})) is not None:
+ if (
+ group_q := await db["groups"].find_one({"_id": ObjectId(group_id)})
+ ) is not None:
+ group = GroupDB.from_mongo(group_q)
+ found_user = None
+ found_user_index = -1
+ for i, u in enumerate(group.users):
+ if u.user.email == username:
+ found_user = u.user
+ found_user_index = i
+ break
+ if found_user and found_user_index >= 0:
+ if role == RoleType.EDITOR:
+ updated_member = Member(user=found_user, editor=True)
+ else:
+ updated_member = Member(user=found_user, editor=False)
+ group.users[found_user_index] = updated_member
+ await db["groups"].replace_one(
+ {"_id": ObjectId(group_id)}, group.to_mongo()
+ )
+ else:
+ raise HTTPException(
+ status_code=404,
+ detail=f"User {username} does not belong to this group!",
+ )
+ return group
+ raise HTTPException(status_code=404, detail=f"Group {group_id} not found")
+ raise HTTPException(status_code=404, detail=f"User {username} not found")
diff --git a/backend/app/tests/test_groups.py b/backend/app/tests/test_groups.py
index 2f3028288..f13909ff4 100644
--- a/backend/app/tests/test_groups.py
+++ b/backend/app/tests/test_groups.py
@@ -7,9 +7,6 @@
create_user,
get_user_token,
)
-from app.models.pyobjectid import PyObjectId
-from app.models.authorization import AuthorizationDB
-
member_alt = {"user": user_alt, "editor": False}
@@ -42,6 +39,51 @@ def test_search_group(client: TestClient, headers: dict):
assert response.status_code == 200
+def test_add_member_with_editor_role(client: TestClient, headers: dict):
+ new_group = create_group(client, headers)
+ group_id = new_group.get("id")
+
+ create_user(client, headers)
+ new_group["users"].append(member_alt)
+
+ response = client.post(
+ f"{settings.API_V2_STR}/groups/{group_id}/add/{member_alt['user']['email']}?role=editor",
+ headers=headers,
+ )
+
+ assert response.status_code == 200
+ assert response.json().get("id") is not None
+ for user in response.json().get("users"):
+ if user.get("user").get("email") == member_alt["user"]["email"]:
+ assert user.get("editor") is True
+
+
+def test_assign_member_role(client: TestClient, headers: dict):
+ new_group = create_group(client, headers)
+ group_id = new_group.get("id")
+
+ create_user(client, headers)
+ new_group["users"].append(member_alt)
+
+ # create user as viewer
+ client.post(
+ f"{settings.API_V2_STR}/groups/{group_id}/add/{member_alt['user']['email']}",
+ headers=headers,
+ )
+
+ # assign as editor
+ response = client.put(
+ f"{settings.API_V2_STR}/groups/{group_id}/update/{member_alt['user']['email']}?role=editor",
+ headers=headers,
+ )
+
+ assert response.status_code == 200
+ assert response.json().get("id") is not None
+ for user in response.json().get("users"):
+ if user.get("user").get("email") == member_alt["user"]["email"]:
+ assert user.get("editor") is True
+
+
def test_member_permissions(client: TestClient, headers: dict):
new_group = create_group(client, headers)
group_id = new_group.get("id")
diff --git a/frontend/src/actions/group.js b/frontend/src/actions/group.js
index e72f8b410..613182b8a 100644
--- a/frontend/src/actions/group.js
+++ b/frontend/src/actions/group.js
@@ -19,6 +19,42 @@ export function fetchGroups(skip=0, limit=21){
};
}
+export const DELETE_GROUP = "DELETE_GROUP";
+export function deleteGroup(groupId){
+ return (dispatch) => {
+ return V2.GroupsService.deleteGroupApiV2GroupsGroupIdDelete(groupId)
+ .then(json => {
+ dispatch({
+ type: DELETE_GROUP,
+ about: json,
+ receivedAt: Date.now(),
+ });
+ })
+ .catch(reason => {
+ dispatch(handleErrors(reason, deleteGroup(groupId)));
+ });
+
+ };
+}
+
+export const SEARCH_GROUPS = "SEARCH_GROUPS";
+export function searchGroups(searchTerm, skip=0, limit=21) {
+ return (dispatch) => {
+ return V2.GroupsService.searchGroupApiV2GroupsSearchSearchTermGet(searchTerm, skip, limit)
+ .then(json => {
+ dispatch({
+ type: SEARCH_GROUPS,
+ groups: json,
+ receivedAt: Date.now(),
+ });
+ })
+ .catch(reason => {
+ dispatch(handleErrors(reason, searchGroups(searchTerm, skip, limit)));
+ });
+
+ };
+}
+
export const RECEIVE_GROUP_ABOUT = "RECEIVE_GROUP_ABOUT";
export function fetchGroupAbout(id){
return (dispatch) => {
@@ -54,9 +90,9 @@ export function deleteGroupMember(groupId, username){
}
export const ADD_GROUP_MEMBER = "ADD_GROUP_MEMBER";
-export function addGroupMember(groupId, username){
+export function addGroupMember(groupId, username, role="viewer"){
return (dispatch) => {
- return V2.GroupsService.addMemberApiV2GroupsGroupIdAddUsernamePost(groupId, username)
+ return V2.GroupsService.addMemberApiV2GroupsGroupIdAddUsernamePost(groupId, username, role)
.then(json => {
dispatch({
type: ADD_GROUP_MEMBER,
@@ -65,7 +101,24 @@ export function addGroupMember(groupId, username){
});
})
.catch(reason => {
- dispatch(handleErrors(reason, addGroupMember(groupId, username)));
+ dispatch(handleErrors(reason, addGroupMember(groupId, username, role)));
+ });
+ };
+}
+
+export const ASSIGN_GROUP_MEMBER_ROLE = "ASSIGN_GROUP_MEMBER_ROLE";
+export function assignGroupMemberRole(groupId, username, role="viewer"){
+ return (dispatch) => {
+ return V2.GroupsService.updateMemberApiV2GroupsGroupIdUpdateUsernamePut(groupId, username, role)
+ .then(json => {
+ dispatch({
+ type: ASSIGN_GROUP_MEMBER_ROLE,
+ about: json,
+ receivedAt: Date.now(),
+ });
+ })
+ .catch(reason => {
+ dispatch(handleErrors(reason, assignGroupMemberRole(groupId, username, role)));
});
};
}
diff --git a/frontend/src/components/groups/AddMemberModal.tsx b/frontend/src/components/groups/AddMemberModal.tsx
index 59e8f13fb..5ddf5e3f4 100644
--- a/frontend/src/components/groups/AddMemberModal.tsx
+++ b/frontend/src/components/groups/AddMemberModal.tsx
@@ -26,7 +26,7 @@ type AddMemberModalProps = {
open: boolean,
handleClose: any,
groupName: string,
- groupId: string,
+ groupId: string|undefined,
}
export default function AddMemberModal(props: AddMemberModalProps) {
diff --git a/frontend/src/components/groups/Group.tsx b/frontend/src/components/groups/Group.tsx
index 30d1cf2dd..743ac47a8 100644
--- a/frontend/src/components/groups/Group.tsx
+++ b/frontend/src/components/groups/Group.tsx
@@ -3,14 +3,17 @@ import {Box, Button} from "@mui/material";
import Layout from "../Layout";
import {RootState} from "../../types/data";
import {useDispatch, useSelector} from "react-redux";
-import {fetchGroupAbout} from "../../actions/group";
+import {deleteGroup, fetchGroupAbout} from "../../actions/group";
import {fetchGroupRole} from "../../actions/authorization";
import Typography from "@mui/material/Typography";
-import {useParams} from "react-router-dom";
+import {useNavigate, useParams} from "react-router-dom";
import {AuthWrapper} from "../auth/AuthWrapper";
import PersonAddAlt1Icon from "@mui/icons-material/PersonAddAlt1";
import MembersTable from "./MembersTable";
import AddMemberModal from "./AddMemberModal";
+import RoleChip from "../auth/RoleChip";
+import DeleteIcon from "@mui/icons-material/Delete";
+import {ActionModal} from "../dialog/ActionModal";
export function Group() {
@@ -18,15 +21,20 @@ export function Group() {
// path parameter
const {groupId} = useParams<{ groupId?: string }>();
+ // use history hook to redirect/navigate between routes
+ const history = useNavigate();
+
// Redux connect equivalent
const dispatch = useDispatch();
const fetchGroupInfo = (groupId: string | undefined) => dispatch(fetchGroupAbout(groupId));
const fetchCurrentGroupRole = (groupId: string | undefined) => dispatch(fetchGroupRole(groupId));
+ const groupDeleted = (groupId: string | undefined) => dispatch(deleteGroup(groupId));
const groupAbout = useSelector((state: RootState) => state.group.about);
const role = useSelector((state: RootState) => state.group.role);
- const [addMemberModalOpen,setAddMemberModalOpen] = useState(false);
+ const [addMemberModalOpen, setAddMemberModalOpen] = useState(false);
+ const [deleteGroupConfirmOpen, setDeleteGroupConfirmOpen] = useState(false);
// component did mount
useEffect(() => {
@@ -36,27 +44,57 @@ export function Group() {
return (
- {setAddMemberModalOpen(false);}}
+ {/*Delete group modal*/}
+ {
+ groupDeleted(groupId);
+ setDeleteGroupConfirmOpen(false);
+ // Go to Explore page
+ history("/");
+ }}
+ handleActionCancel={() => {
+ setDeleteGroupConfirmOpen(false);
+ }}/>
+ {setAddMemberModalOpen(false);}}
groupName={groupAbout.name} groupId={groupAbout.id}/>
-
-
- {groupAbout !== undefined ? groupAbout.name : "Not found"}
-
- {groupAbout.description}
+
+
+
+
+
+ {groupAbout !== undefined ? groupAbout.name : "Not found"}
+
+
+
+ {groupAbout.description}
+
+
+
+
+ {/*Buttons*/}
+
+ {/*only owner or editor are allowed to edit*/}
+
+
+
+ {/*only owner are allowed to delete*/}
+
+
+
-
-
-
diff --git a/frontend/src/components/groups/Groups.tsx b/frontend/src/components/groups/Groups.tsx
index 9bd70cae3..904dfa135 100644
--- a/frontend/src/components/groups/Groups.tsx
+++ b/frontend/src/components/groups/Groups.tsx
@@ -1,51 +1,54 @@
import React, {useEffect, useState} from "react";
-import {
- Box,
- Button,
- ButtonGroup, CardActionArea,
- Divider,
- Grid,
- List,
-} from "@mui/material";
-import Layout from "../Layout";
+import {Box, Button, ButtonGroup, Grid, IconButton, InputBase,} from "@mui/material";
import {RootState} from "../../types/data";
import {useDispatch, useSelector} from "react-redux";
-import {fetchGroups} from "../../actions/group";
-import {ArrowBack, ArrowForward} from "@material-ui/icons";
-import Typography from '@mui/material/Typography';
-import {theme} from "../../theme";
+import {fetchGroups, searchGroups as searchGroupsAction} from "../../actions/group";
+import {ArrowBack, ArrowForward, SearchOutlined} from "@material-ui/icons";
import {Link} from "react-router-dom";
-import Card from "@mui/material/Card";
-import CardContent from "@mui/material/CardContent";
+import Paper from "@mui/material/Paper";
+import Table from "@mui/material/Table";
+import TableHead from "@mui/material/TableHead";
+import TableRow from "@mui/material/TableRow";
+import TableCell from "@mui/material/TableCell";
+import TableBody from "@mui/material/TableBody";
+import TableContainer from "@mui/material/TableContainer";
+import GroupsIcon from "@mui/icons-material/Groups";
+import {theme} from "../../theme";
export function Groups() {
// Redux connect equivalent
const dispatch = useDispatch();
- const listGroups = (skip: number | undefined, limit: number | undefined) =>
- dispatch(fetchGroups(skip, limit));
+ const listGroups = (skip: number | undefined, limit: number | undefined) => dispatch(fetchGroups(skip, limit));
+ const searchGroups = (searchTerm: string, skip: number | undefined, limit: number | undefined) => dispatch(searchGroupsAction(searchTerm, skip, limit));
const groups = useSelector((state: RootState) => state.group.groups);
- // TODO add option to determine limit number; default show 5 datasets each time
+ // TODO add option to determine limit number; default show 5 groups each time
const [currPageNum, setCurrPageNum] = useState(0);
const [limit,] = useState(20);
- const [skip, setSkip] = useState();
+ const [skip, setSkip] = useState(0);
const [prevDisabled, setPrevDisabled] = useState(true);
const [nextDisabled, setNextDisabled] = useState(false);
+ const [searchTerm, setSearchTerm] = useState("");
// component did mount
useEffect(() => {
listGroups(skip, limit);
}, []);
- // fetch extractors from each individual dataset/id calls
useEffect(() => {
// disable flipping if reaches the last page
if (groups.length < limit) setNextDisabled(true);
else setNextDisabled(false);
}, [groups]);
+ // search
+ useEffect(() => {
+ if (searchTerm !== "") searchGroups(searchTerm, skip, limit);
+ else listGroups(skip, limit);
+ }, [searchTerm]);
+
useEffect(() => {
if (skip !== null && skip !== undefined) {
listGroups(skip, limit);
@@ -54,7 +57,6 @@ export function Groups() {
}
}, [skip]);
- // for pagination keep flipping until the return dataset is less than the limit
const previous = () => {
if (currPageNum - 1 >= 0) {
setSkip((currPageNum - 1) * limit);
@@ -69,57 +71,87 @@ export function Groups() {
};
return (
-
-
-
-
+
+ {/*searchbox*/}
+
-
+ width: "80%"
+ }}
+ >
+ {
+ setSearchTerm(e.target.value);
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ searchGroups(searchTerm, skip, limit);
+ }
+ }}
+ value={searchTerm}
+ />
+ {
+ searchGroups(searchTerm, skip, limit);
+ }}>
+
+
+
+
+
+
+
+
+
+ Group Name
+ Description
+
+
+
+
{
- groups !== undefined ?
- groups.map((group) => {
- return (<>
-
-
-
- {group.name}
-
- Size: {group.users !== undefined ? group.users.length : 0}
-
-
- {group.description}
-
-
-
-
-
- >);
- })
- :
- <>>
+ groups.map((group) => {
+ return (
+
+
+
+
+
+ {group.description}
+
+
+ {group.users !== undefined ? group.users.length : 0}
+
+
+ )
+ })
}
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
-
+
);
}
diff --git a/frontend/src/components/groups/MembersTable.tsx b/frontend/src/components/groups/MembersTable.tsx
index 2197767a5..96f88c566 100644
--- a/frontend/src/components/groups/MembersTable.tsx
+++ b/frontend/src/components/groups/MembersTable.tsx
@@ -28,23 +28,21 @@ export default function MembersTable(props: MembersTableProps) {
const dispatch = useDispatch();
const groupMemberDeleted = (groupId: string|undefined, username: string|undefined) => dispatch(deleteGroupMember(groupId, username))
- // use history hook to redirect/navigate between routes
- const [deleteDatasetConfirmOpen, setDeleteDatasetConfirmOpen] = useState(false);
+ const [deleteMemberConfirmOpen, setDeleteMemberConfirmOpen] = useState(false);
const [selectMemberUsername, setSelectMemberUsername] = useState();
return (
<>
- {
groupMemberDeleted(groupId, selectMemberUsername);
- setDeleteDatasetConfirmOpen(false);
+ setDeleteMemberConfirmOpen(false);
}}
handleActionCancel={() => {
- setDeleteDatasetConfirmOpen(false);
+ setDeleteMemberConfirmOpen(false);
}}/>
-
@@ -60,9 +58,10 @@ export default function MembersTable(props: MembersTableProps) {
groupAbout !== undefined && groupAbout.users !== undefined?
(groupAbout.users.map((member) => (
)))
diff --git a/frontend/src/components/groups/MembersTableUserEntry.tsx b/frontend/src/components/groups/MembersTableUserEntry.tsx
index 22ef9decb..71d2084ce 100644
--- a/frontend/src/components/groups/MembersTableUserEntry.tsx
+++ b/frontend/src/components/groups/MembersTableUserEntry.tsx
@@ -1,19 +1,25 @@
-import React from "react";
+import React, {useEffect, useState} from "react";
import TableRow from "@mui/material/TableRow";
import TableCell from "@mui/material/TableCell";
import PersonIcon from "@mui/icons-material/Person";
-import {Button, IconButton} from "@mui/material";
+import {Button, ButtonGroup, FormControl, IconButton, InputLabel, MenuItem, Select} from "@mui/material";
import {Member} from "../../openapi/v2";
import DeleteIcon from "@mui/icons-material/Delete";
import {theme} from "../../theme";
import Gravatar from "react-gravatar";
-
+import {AuthWrapper} from "../auth/AuthWrapper";
+import {useDispatch, useSelector} from "react-redux";
+import {RootState} from "../../types/data";
+import {assignGroupMemberRole} from "../../actions/group";
+import EditIcon from "@mui/icons-material/Edit"
+import CheckIcon from "@mui/icons-material/Check";
+import CloseIcon from "@mui/icons-material/Close";
type MembersTableUserEntryProps = {
- iconStyle: {}
+ groupId: string|undefined
member: Member
- setDeleteDatasetConfirmOpen: any
+ setDeleteMemberConfirmOpen: any
setSelectMemberUsername: any
}
@@ -25,12 +31,40 @@ const iconStyle = {
export function MembersTableUserEntry(props: MembersTableUserEntryProps) {
- const {member, setDeleteDatasetConfirmOpen, setSelectMemberUsername} = props;
+ const {groupId, member, setDeleteMemberConfirmOpen, setSelectMemberUsername} = props;
+
+ const dispatch = useDispatch();
+ const groupMemberRoleAssigned = (groupId: string|undefined, username: string|undefined,
+ role: string|undefined) => dispatch(assignGroupMemberRole(groupId, username, role));
+ const role = useSelector((state: RootState) => state.group.role);
+
+ const [selectedRole, setSelectedRole] = useState(member.editor ? "editor": "member");
+ const [editRoleOn, setEditRoleOn] = useState(false);
+
+ // if any thing updated in redux, reflect on the selected member
+ useEffect(() => {
+ setSelectedRole(member.editor ? "editor" : "member");
+ }, [member]);
+
+ const handleRoleSelection = (e) => {
+ setSelectedRole(e.target.value);
+ };
+
+ // Resume to the current state in redux
+ const handleRoleCancel = () => {
+ setSelectedRole(member.editor ? "editor" : "member");
+ setEditRoleOn(false);
+ };
+
+ const handleRoleSave = () => {
+ groupMemberRoleAssigned(groupId, member.user.email, selectedRole);
+ setEditRoleOn(false);
+ };
return (
{
@@ -43,14 +77,57 @@ export function MembersTableUserEntry(props: MembersTableUserEntryProps) {
{member.user.email}
- {member.editor !== undefined && member.editor ? "Editor" : "Member"}
- {
- setSelectMemberUsername(member.user.email)
- setDeleteDatasetConfirmOpen(true);
- }}>
-
-
+ {
+ editRoleOn ?
+
+ Role
+
+
+ :
+ member.editor !== undefined && member.editor ?
+ "Editor" : "Member"
+ }
+ {/*only owner or editor are allowed to modify roles of the member*/}
+
+ {
+ editRoleOn ?
+
+
+
+
+
+
+
+
+ :
+ {
+ setEditRoleOn(true);
+ }}>
+
+
+ }
+
+
+
+ {/*only owner or editor are allowed to delete*/}
+
+ {
+ setSelectMemberUsername(member.user.email)
+ setDeleteMemberConfirmOpen(true);
+ }}>
+
+
+
)
diff --git a/frontend/src/openapi/v2/services/GroupsService.ts b/frontend/src/openapi/v2/services/GroupsService.ts
index 11dca199a..b7eb76e86 100644
--- a/frontend/src/openapi/v2/services/GroupsService.ts
+++ b/frontend/src/openapi/v2/services/GroupsService.ts
@@ -58,6 +58,38 @@ export class GroupsService {
});
}
+ /**
+ * Search Group
+ * Search all groups in the db based on text.
+ *
+ * Arguments:
+ * text -- any text matching name or description
+ * skip -- number of initial records to skip (i.e. for pagination)
+ * limit -- restrict number of records to be returned (i.e. for pagination)
+ * @param searchTerm
+ * @param skip
+ * @param limit
+ * @returns GroupOut Successful Response
+ * @throws ApiError
+ */
+ public static searchGroupApiV2GroupsSearchSearchTermGet(
+ searchTerm: string,
+ skip?: number,
+ limit: number = 10,
+ ): CancelablePromise> {
+ return __request({
+ method: 'GET',
+ path: `/api/v2/groups/search/${searchTerm}`,
+ query: {
+ 'skip': skip,
+ 'limit': limit,
+ },
+ errors: {
+ 422: `Validation Error`,
+ },
+ });
+ }
+
/**
* Get Group
* @param groupId
@@ -101,12 +133,12 @@ export class GroupsService {
/**
* Delete Group
* @param groupId
- * @returns any Successful Response
+ * @returns GroupOut Successful Response
* @throws ApiError
*/
public static deleteGroupApiV2GroupsGroupIdDelete(
groupId: string,
- ): CancelablePromise {
+ ): CancelablePromise {
return __request({
method: 'DELETE',
path: `/api/v2/groups/${groupId}`,
@@ -117,17 +149,25 @@ export class GroupsService {
}
/**
- * Search Group
- * @param searchTerm
- * @returns any Successful Response
+ * Add Member
+ * Add a new user to a group.
+ * @param groupId
+ * @param username
+ * @param role
+ * @returns GroupOut Successful Response
* @throws ApiError
*/
- public static searchGroupApiV2GroupsSearchSearchTermGet(
- searchTerm: string,
- ): CancelablePromise {
+ public static addMemberApiV2GroupsGroupIdAddUsernamePost(
+ groupId: string,
+ username: string,
+ role?: string,
+ ): CancelablePromise {
return __request({
- method: 'GET',
- path: `/api/v2/groups/search/${searchTerm}`,
+ method: 'POST',
+ path: `/api/v2/groups/${groupId}/add/${username}`,
+ query: {
+ 'role': role,
+ },
errors: {
422: `Validation Error`,
},
@@ -135,20 +175,20 @@ export class GroupsService {
}
/**
- * Add Member
- * Add a new user to a group.
+ * Remove Member
+ * Remove a user from a group.
* @param groupId
* @param username
* @returns any Successful Response
* @throws ApiError
*/
- public static addMemberApiV2GroupsGroupIdAddUsernamePost(
+ public static removeMemberApiV2GroupsGroupIdRemoveUsernamePost(
groupId: string,
username: string,
): CancelablePromise {
return __request({
method: 'POST',
- path: `/api/v2/groups/${groupId}/add/${username}`,
+ path: `/api/v2/groups/${groupId}/remove/${username}`,
errors: {
422: `Validation Error`,
},
@@ -156,20 +196,25 @@ export class GroupsService {
}
/**
- * Remove Member
- * Remove a user from a group.
+ * Update Member
+ * Update user role.
* @param groupId
* @param username
- * @returns any Successful Response
+ * @param role
+ * @returns GroupOut Successful Response
* @throws ApiError
*/
- public static removeMemberApiV2GroupsGroupIdRemoveUsernamePost(
+ public static updateMemberApiV2GroupsGroupIdUpdateUsernamePut(
groupId: string,
username: string,
- ): CancelablePromise {
+ role: string,
+ ): CancelablePromise {
return __request({
- method: 'POST',
- path: `/api/v2/groups/${groupId}/remove/${username}`,
+ method: 'PUT',
+ path: `/api/v2/groups/${groupId}/update/${username}`,
+ query: {
+ 'role': role,
+ },
errors: {
422: `Validation Error`,
},
diff --git a/frontend/src/reducers/group.ts b/frontend/src/reducers/group.ts
index 05d47a2f6..467ae79ec 100644
--- a/frontend/src/reducers/group.ts
+++ b/frontend/src/reducers/group.ts
@@ -1,6 +1,6 @@
import {
RECEIVE_GROUPS,
- RECEIVE_GROUP_ABOUT, DELETE_GROUP_MEMBER, ADD_GROUP_MEMBER,
+ RECEIVE_GROUP_ABOUT, DELETE_GROUP_MEMBER, ADD_GROUP_MEMBER, ASSIGN_GROUP_MEMBER_ROLE, DELETE_GROUP, SEARCH_GROUPS,
} from "../actions/group";
import {
RECEIVE_GROUP_ROLE
@@ -21,6 +21,8 @@ const group = (state = defaultState, action: DataAction) => {
switch (action.type) {
case RECEIVE_GROUPS:
return Object.assign({}, state, {groups: action.groups});
+ case SEARCH_GROUPS:
+ return Object.assign({}, state, {groups: action.groups});
case RECEIVE_GROUP_ABOUT:
return Object.assign({}, state, {about: action.about});
case RECEIVE_GROUP_ROLE:
@@ -31,6 +33,12 @@ const group = (state = defaultState, action: DataAction) => {
return Object.assign({}, state, {about: action.about});
case LIST_USERS:
return Object.assign({}, state, {users: action.users});
+ case ASSIGN_GROUP_MEMBER_ROLE:
+ return Object.assign({}, state, {about: action.about});
+ case DELETE_GROUP:
+ return Object.assign({}, state, {
+ groups: state.groups.filter(group => group.id !== action.about.id),
+ });
default:
return state;
}
diff --git a/frontend/src/types/action.ts b/frontend/src/types/action.ts
index 9e10dbdb9..fd87b8b7d 100644
--- a/frontend/src/types/action.ts
+++ b/frontend/src/types/action.ts
@@ -10,6 +10,7 @@ import {
import {MetadataDefinitionOut as MetadataDefinition} from "../openapi/v2";
import {GroupAndRole, UserAndRole} from "../openapi/v2";
import {LIST_USERS} from "../actions/user";
+import {DELETE_GROUP} from "../actions/group";
interface RECEIVE_FILES_IN_DATASET {
type: "RECEIVE_FILES_IN_DATASET";
@@ -273,6 +274,16 @@ interface RECEIVE_GROUPS{
groups: Group[];
}
+interface SEARCH_GROUPS{
+ type: "SEARCH_GROUPS"
+ groups: Group[];
+}
+
+interface DELETE_GROUP{
+ type: "DELETE_GROUP"
+ about: Group;
+}
+
interface RECEIVE_GROUP_ABOUT{
type: "RECEIVE_GROUP_ABOUT"
about: Group;
@@ -297,6 +308,10 @@ interface LIST_USERS{
type: "LIST_USERS"
users: UserOut[]
}
+interface ASSIGN_GROUP_MEMBER_ROLE{
+ type: "ASSIGN_GROUP_MEMBER_ROLE"
+ about: Group
+}
export type DataAction =
| RECEIVE_FILES_IN_DATASET
@@ -349,10 +364,13 @@ export type DataAction =
| FETCH_JOB_SUMMARY
| FETCH_JOB_UPDATES
| RECEIVE_GROUPS
+ | SEARCH_GROUPS
+ | DELETE_GROUP
| RECEIVE_GROUP_ABOUT
| RECEIVE_GROUP_ROLE
| DELETE_GROUP_MEMBER
| ADD_GROUP_MEMBER
+ | ASSIGN_GROUP_MEMBER_ROLE
| LIST_USERS
| RECEIVE_DATASET_GROUPS_AND_ROLES
| RECEIVE_DATASET_USERS_AND_ROLES