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