-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
task/FP-1131: Add Member role to shared workspace (frontend) (#597)
* Add API endpoints to update project/system roles for users * Additional backend changes to support project/system role queries * Decouple project/system role selectors in the Manage Team Members modal * use single-column view that only displays the Tapis role * Add new users as USER instead of ADMIN * fix cutoff for ownership transfer text * Restore memoization of table data * Hide roles column for USER/MEMBER since they can't see others' roles * python linting * Fix memoization loop causing infinite query refetch * hide role column while transferring ownership * fix(FP-1670): button text overflow (workspaces) (#641) * merge main * show spinner while mutating Co-authored-by: Sal Tijerina <r.sal.tijerina@gmail.com> Co-authored-by: Wesley B <62723358+wesleyboar@users.noreply.github.com>
- Loading branch information
1 parent
f33e483
commit b3aa730
Showing
21 changed files
with
733 additions
and
64 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
86 changes: 86 additions & 0 deletions
86
client/src/components/DataFiles/DataFilesProjectMembers/_cells/ProjectRoleSelector.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import React, { useState, useEffect, useContext, createContext } from 'react'; | ||
import { useQuery, useMutation, useQueryClient } from 'react-query'; | ||
import Cookies from 'js-cookie'; | ||
import fetch from 'cross-fetch'; | ||
import DropdownSelector from '_common/DropdownSelector'; | ||
import { Button } from 'reactstrap'; | ||
import LoadingSpinner from '_common/LoadingSpinner'; | ||
|
||
const getProjectRole = async (projectId, username) => { | ||
const url = `/api/projects/${projectId}/project-role/${username}/`; | ||
const request = await fetch(url, { | ||
headers: { 'X-CSRFToken': Cookies.get('csrftoken') }, | ||
credentials: 'same-origin', | ||
}); | ||
const data = await request.json(); | ||
return data; | ||
}; | ||
|
||
const setProjectRole = async (projectId, username, oldRole, newRole) => { | ||
const url = `/api/projects/${projectId}/members/`; | ||
const request = await fetch(url, { | ||
headers: { 'X-CSRFToken': Cookies.get('csrftoken') }, | ||
credentials: 'same-origin', | ||
method: 'PATCH', | ||
body: JSON.stringify({ | ||
action: 'change_project_role', | ||
username, | ||
oldRole, | ||
newRole, | ||
}), | ||
}); | ||
const data = await request.json(); | ||
return data; | ||
}; | ||
|
||
const useProjectRole = (projectId, username) => { | ||
const queryClient = useQueryClient(); | ||
const query = useQuery(['project-role', projectId, username], () => | ||
getProjectRole(projectId, username) | ||
); | ||
const mutation = useMutation(async ({ oldRole, newRole }) => { | ||
await setProjectRole(projectId, username, oldRole, newRole); | ||
query.refetch(); | ||
// Invalidate the system role query to keep it up to date. | ||
queryClient.invalidateQueries(['system-role', projectId, username]); | ||
}); | ||
return { query, mutation }; | ||
}; | ||
|
||
const ProjectRoleSelector = ({ projectId, username }) => { | ||
const { | ||
query: { data, isLoading, error, isFetching }, | ||
mutation: { mutate: setProjectRole, isLoading: isMutating }, | ||
} = useProjectRole(projectId, username); | ||
|
||
const [selectedRole, setSelectedRole] = useState(data?.role); | ||
useEffect(() => setSelectedRole(data?.role), [data?.role]); | ||
|
||
if (isLoading) return <LoadingSpinner placement="inline" />; | ||
if (error) return <span>Error</span>; | ||
if (data?.role == 'pi') return <span>PI</span>; | ||
return ( | ||
<div style={{ display: 'inline-flex' }}> | ||
<DropdownSelector | ||
data-testid="role-dropdown" | ||
value={selectedRole} | ||
onChange={(e) => setSelectedRole(e.target.value)} | ||
> | ||
<option value="co_pi">Co-PI</option> | ||
<option value="team_member">Member</option> | ||
</DropdownSelector> | ||
{data.role !== selectedRole && !isFetching && ( | ||
<Button | ||
className="data-files-btn" | ||
onClick={() => | ||
setProjectRole({ oldRole: data.role, newRole: selectedRole }) | ||
} | ||
> | ||
{isMutating ? <LoadingSpinner placement="inline" /> : 'Update'} | ||
</Button> | ||
)} | ||
</div> | ||
); | ||
}; | ||
|
||
export default ProjectRoleSelector; |
100 changes: 100 additions & 0 deletions
100
client/src/components/DataFiles/DataFilesProjectMembers/_cells/SystemRoleSelector.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import React, { useState, useEffect, createContext, useContext } from 'react'; | ||
import { useQuery, useMutation } from 'react-query'; | ||
import { useSelector } from 'react-redux'; | ||
import Cookies from 'js-cookie'; | ||
import fetch from 'cross-fetch'; | ||
import DropdownSelector from '_common/DropdownSelector'; | ||
import { Button } from 'reactstrap'; | ||
import styles from '../DataFilesProjectMembers.module.scss'; | ||
import LoadingSpinner from '_common/LoadingSpinner'; | ||
|
||
const getSystemRole = async (projectId, username) => { | ||
const url = `/api/projects/${projectId}/system-role/${username}/`; | ||
const request = await fetch(url, { | ||
headers: { 'X-CSRFToken': Cookies.get('csrftoken') }, | ||
credentials: 'same-origin', | ||
}); | ||
const data = await request.json(); | ||
return data; | ||
}; | ||
|
||
const setSystemRole = async (projectId, username, role) => { | ||
const url = `/api/projects/${projectId}/members/`; | ||
const request = await fetch(url, { | ||
headers: { 'X-CSRFToken': Cookies.get('csrftoken') }, | ||
credentials: 'same-origin', | ||
method: 'PATCH', | ||
body: JSON.stringify({ | ||
action: 'change_system_role', | ||
username, | ||
newRole: role, | ||
}), | ||
}); | ||
const data = await request.json(); | ||
return data; | ||
}; | ||
|
||
export const useSystemRole = (projectId, username) => { | ||
const query = useQuery(['system-role', projectId, username], () => | ||
getSystemRole(projectId, username) | ||
); | ||
const mutation = useMutation(async (role) => { | ||
await setSystemRole(projectId, username, role); | ||
query.refetch(); | ||
}); | ||
return { query, mutation }; | ||
}; | ||
|
||
const SystemRoleSelector = ({ projectId, username }) => { | ||
const authenticatedUser = useSelector( | ||
(state) => state.authenticatedUser.user.username | ||
); | ||
const { query: authenticatedUserQuery } = useSystemRole( | ||
projectId, | ||
authenticatedUser | ||
); | ||
const currentUserRole = authenticatedUserQuery.data?.role; | ||
|
||
const { | ||
query: { data, isLoading, isFetching, error }, | ||
mutation: { mutate: setSystemRole, isLoading: isMutating }, | ||
} = useSystemRole(projectId, username); | ||
const [selectedRole, setSelectedRole] = useState(data?.role); | ||
useEffect(() => setSelectedRole(data?.role), [data?.role]); | ||
|
||
if (isLoading || authenticatedUserQuery.isLoading || isMutating) | ||
return <LoadingSpinner placement="inline" />; | ||
if (error) return <span>Error</span>; | ||
//Only owners/admins can change roles; | ||
// owner roles cannot be changed except using the Transfer mechanism; | ||
// users cannot change their own roles. | ||
if ( | ||
data.role === 'OWNER' || | ||
username === authenticatedUser || | ||
!['OWNER', 'ADMIN'].includes(currentUserRole) | ||
) | ||
return <span>{data.role}</span>; | ||
return ( | ||
<div style={{ display: 'inline-flex' }}> | ||
<DropdownSelector | ||
value={selectedRole} | ||
onChange={(e) => setSelectedRole(e.target.value)} | ||
> | ||
{username !== authenticatedUser && <option value="ADMIN">ADMIN</option>} | ||
<option value="USER">USER</option> | ||
<option value="GUEST">GUEST</option> | ||
</DropdownSelector> | ||
{data.role !== selectedRole && !isFetching && ( | ||
<Button | ||
style={{ marginLeft: '5px' }} | ||
className={styles['ownership-button']} | ||
onClick={() => setSystemRole(selectedRole)} | ||
> | ||
Update | ||
</Button> | ||
)} | ||
</div> | ||
); | ||
}; | ||
|
||
export default SystemRoleSelector; |
38 changes: 38 additions & 0 deletions
38
...rc/components/DataFiles/DataFilesProjectMembers/_cells/_tests/ProjectRoleSelector.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import React from 'react'; | ||
import fetchMock from 'fetch-mock'; | ||
import configureStore from 'redux-mock-store'; | ||
import '@testing-library/jest-dom/extend-expect'; | ||
import renderComponent from 'utils/testing'; | ||
import ProjectRoleSelector from '../ProjectRoleSelector'; | ||
import { waitFor, screen, fireEvent } from '@testing-library/react'; | ||
|
||
import fetch from 'cross-fetch'; | ||
jest.mock('cross-fetch'); | ||
const mockStore = configureStore(); | ||
|
||
describe('ProjectRoleSelector', () => { | ||
it('renders AppRouter and dispatches events', async () => { | ||
const fm = fetchMock | ||
.sandbox() | ||
.get(`/api/projects/CEP-000/project-role/testuser/`, { | ||
status: 200, | ||
body: { role: 'co_pi' }, | ||
}); | ||
fetch.mockImplementation(fm); | ||
|
||
renderComponent( | ||
<ProjectRoleSelector projectId="CEP-000" username="testuser" />, | ||
mockStore({}) | ||
); | ||
expect(await screen.findByTestId('loading-spinner')).toBeDefined(); | ||
|
||
await waitFor(async () => { | ||
expect(await screen.findByDisplayValue('Co-PI')).toBeDefined(); | ||
const selector = await screen.findByTestId('selector'); | ||
|
||
fireEvent.change(selector, { target: { value: 'team_member' } }); | ||
expect(await screen.findByDisplayValue('Member')).toBeDefined(); | ||
expect(await screen.findByText('Update')).toBeDefined(); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.