From 8e27e0ce28afe6025e6407b30978ab6c706ce9f2 Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Tue, 28 Jul 2020 13:16:55 -0400 Subject: [PATCH] Teams Access List using Resource Access component --- awx/ui_next/src/api/models/Teams.js | 3 +- awx/ui_next/src/screens/Team/Team.jsx | 4 +- .../Team/TeamUsers/TeamUserListItem.jsx | 102 ------- .../Team/TeamUsers/TeamUserListItem.test.jsx | 84 ------ .../screens/Team/TeamUsers/TeamUsersList.jsx | 198 ------------- .../Team/TeamUsers/TeamUsersList.test.jsx | 261 ------------------ .../src/screens/Team/TeamUsers/index.js | 4 - 7 files changed, 3 insertions(+), 653 deletions(-) delete mode 100644 awx/ui_next/src/screens/Team/TeamUsers/TeamUserListItem.jsx delete mode 100644 awx/ui_next/src/screens/Team/TeamUsers/TeamUserListItem.test.jsx delete mode 100644 awx/ui_next/src/screens/Team/TeamUsers/TeamUsersList.jsx delete mode 100644 awx/ui_next/src/screens/Team/TeamUsers/TeamUsersList.test.jsx delete mode 100644 awx/ui_next/src/screens/Team/TeamUsers/index.js diff --git a/awx/ui_next/src/api/models/Teams.js b/awx/ui_next/src/api/models/Teams.js index e19400ad5b..1a205993d4 100644 --- a/awx/ui_next/src/api/models/Teams.js +++ b/awx/ui_next/src/api/models/Teams.js @@ -29,7 +29,7 @@ class Teams extends Base { return this.http.options(`${this.baseUrl}${teamId}/roles/`); } - readUsersAccess(teamId, params) { + readAccessList(teamId, params) { return this.http.get(`${this.baseUrl}${teamId}/access_list/`, { params, }); @@ -38,7 +38,6 @@ class Teams extends Base { readUsersAccessOptions(teamId) { return this.http.options(`${this.baseUrl}${teamId}/users/`); } - } export default Teams; diff --git a/awx/ui_next/src/screens/Team/Team.jsx b/awx/ui_next/src/screens/Team/Team.jsx index ca04326380..60b71a6a55 100644 --- a/awx/ui_next/src/screens/Team/Team.jsx +++ b/awx/ui_next/src/screens/Team/Team.jsx @@ -17,7 +17,7 @@ import TeamDetail from './TeamDetail'; import TeamEdit from './TeamEdit'; import { TeamsAPI } from '../../api'; import TeamAccessList from './TeamAccess'; -import TeamUsersList from './TeamUsers'; +import { ResourceAccessList } from '../../components/ResourceAccessList'; function Team({ i18n, setBreadcrumb }) { const [team, setTeam] = useState(null); @@ -97,7 +97,7 @@ function Team({ i18n, setBreadcrumb }) { )} {team && ( - + )} {team && ( diff --git a/awx/ui_next/src/screens/Team/TeamUsers/TeamUserListItem.jsx b/awx/ui_next/src/screens/Team/TeamUsers/TeamUserListItem.jsx deleted file mode 100644 index 916f926b6d..0000000000 --- a/awx/ui_next/src/screens/Team/TeamUsers/TeamUserListItem.jsx +++ /dev/null @@ -1,102 +0,0 @@ -import 'styled-components/macro'; -import React from 'react'; -import { string, func } from 'prop-types'; -import { withI18n } from '@lingui/react'; -import { t } from '@lingui/macro'; -import { - DataListItem, - DataListItemCells, - DataListItemRow, - Label as PFLabel, -} from '@patternfly/react-core'; -import { Link } from 'react-router-dom'; -import styled from 'styled-components'; -import DataListCell from '../../../components/DataListCell'; - -import { User } from '../../../types'; - -function TeamUserListItem({ user, disassociateRole, detailUrl, i18n }) { - const labelId = `check-action-${user.id}`; - const Label = styled.b` - margin-right: 20px; - `; - const hasDirectRoles = user.summary_fields.direct_access.length > 0; - const hasIndirectRoles = user.summary_fields.indirect_access.length > 0; - return ( - - - - - {user.username} - - , - - {user.first_name && ( - <> - - {user.first_name} - - )} - , - - {user.last_name && ( - <> - - {user.last} - - )} - , - - {hasDirectRoles && ( - <> - - - {user.summary_fields.direct_access.map(role => - role.role.name !== 'Read' ? ( - disassociateRole(role.role)} - > - {role.role.name} - - ) : null - )} - - - )} - , - - {hasIndirectRoles && ( - <> - - - {user.summary_fields.indirect_access.map(role => ( - - {role.role.name} - - ))} - - - )} - , - ]} - /> - - - ); -} - -TeamUserListItem.propTypes = { - user: User.isRequired, - detailUrl: string.isRequired, - disassociateRole: func.isRequired, -}; - -export default withI18n()(TeamUserListItem); diff --git a/awx/ui_next/src/screens/Team/TeamUsers/TeamUserListItem.test.jsx b/awx/ui_next/src/screens/Team/TeamUsers/TeamUserListItem.test.jsx deleted file mode 100644 index 91d770e97e..0000000000 --- a/awx/ui_next/src/screens/Team/TeamUsers/TeamUserListItem.test.jsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; - -import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; - -import TeamUserListItem from './TeamUserListItem'; - -describe('', () => { - const user = { - id: 1, - name: 'Team 1', - summary_fields: { - direct_access: [ - { - role: { - id: 40, - name: 'Member', - description: 'User is a member of the team', - resource_name: ' Team 1 Org 0', - resource_type: 'team', - related: { - team: '/api/v2/teams/1/', - }, - user_capabilities: { - unattach: true, - }, - }, - descendant_roles: ['member_role', 'read_role'], - }, - ], - indirect_access: [ - { - role: { - id: 2, - name: 'Admin', - description: 'Can manage all aspects of the organization', - resource_name: ' Organization 0', - resource_type: 'organization', - related: { - organization: '/api/v2/organizations/1/', - }, - user_capabilities: { - unattach: true, - }, - }, - descendant_roles: ['admin_role', 'member_role', 'read_role'], - }, - ], - user_capabilities: { - edit: true, - }, - }, - username: 'Casey', - firstname: 'The', - lastname: 'Cat', - email: '', - }; - test('initially renders succesfully', () => { - mountWithContexts( - {}} - /> - ); - }); - test('initially render prop items', () => { - const wrapper = mountWithContexts( - {}} - /> - ); - expect(wrapper.find('DataListCell[aria-label="username"]').length).toBe(1); - expect(wrapper.find('DataListCell[aria-label="first name"]').length).toBe( - 1 - ); - expect(wrapper.find('DataListCell[aria-label="last name"]').length).toBe(1); - expect(wrapper.find('DataListCell[aria-label="roles"]').length).toBe(1); - expect( - wrapper.find('DataListCell[aria-label="indirect role"]').length - ).toBe(1); - }); -}); diff --git a/awx/ui_next/src/screens/Team/TeamUsers/TeamUsersList.jsx b/awx/ui_next/src/screens/Team/TeamUsers/TeamUsersList.jsx deleted file mode 100644 index d85b3512d2..0000000000 --- a/awx/ui_next/src/screens/Team/TeamUsers/TeamUsersList.jsx +++ /dev/null @@ -1,198 +0,0 @@ -import React, { useEffect, useCallback, useState } from 'react'; -import { useLocation, useRouteMatch, useParams } from 'react-router-dom'; -import { withI18n } from '@lingui/react'; -import { t } from '@lingui/macro'; - -import { Button } from '@patternfly/react-core'; -import { TeamsAPI, UsersAPI } from '../../../api'; -import useRequest, { useDeleteItems } from '../../../util/useRequest'; -import AlertModal from '../../../components/AlertModal'; -import DataListToolbar from '../../../components/DataListToolbar'; -import ErrorDetail from '../../../components/ErrorDetail'; -import PaginatedDataList, { - ToolbarAddButton, -} from '../../../components/PaginatedDataList'; -import { getQSConfig, parseQueryString } from '../../../util/qs'; - -import TeamUserListItem from './TeamUserListItem'; - -const QS_CONFIG = getQSConfig('user', { - page: 1, - page_size: 20, - order_by: 'username', -}); - -function TeamUsersList({ i18n }) { - const location = useLocation(); - const match = useRouteMatch(); - const { id: teamId } = useParams(); - const [roleToDisassociate, setRoleToDisassociate] = useState([]); - - const { - result: { users, itemCount, actions }, - error: contentError, - isLoading, - request: fetchRoles, - } = useRequest( - useCallback(async () => { - const params = parseQueryString(QS_CONFIG, location.search); - const [response, actionsResponse] = await Promise.all([ - TeamsAPI.readUsersAccess(teamId, params), - TeamsAPI.readUsersAccessOptions(teamId), - ]); - return { - users: response.data.results, - itemCount: response.data.count, - actions: actionsResponse.data.actions, - }; - }, [location, teamId]), - { - users: [], - itemCount: 0, - actions: {}, - } - ); - - useEffect(() => { - fetchRoles(); - }, [fetchRoles]); - - const { - isLoading: isDeleteLoading, - deleteItems: disassociateRole, - deletionError, - clearDeletionError, - } = useDeleteItems( - useCallback(async () => { - UsersAPI.disassociateRole( - roleToDisassociate[0].id, - roleToDisassociate[1].id - ); - }, [roleToDisassociate]), - { - qsConfig: QS_CONFIG, - fetchItems: fetchRoles, - } - ); - - const handleRoleDisassociation = async () => { - await disassociateRole(); - setRoleToDisassociate(null); - }; - - const hasContentLoading = isDeleteLoading || isLoading; - const canAdd = actions && actions.POST; - return ( - <> - ( - ] - : []), - ]} - /> - )} - renderItem={user => ( - setRoleToDisassociate([user, role])} - /> - )} - emptyStateControls={ - canAdd ? ( - - ) : null - } - /> - {roleToDisassociate?.length > 0 && ( - setRoleToDisassociate(null)} - actions={[ - , - , - ]} - > -
{i18n._(t`This action will disassociate the following:`)}
- {roleToDisassociate.name} -
- )} - - {i18n._(t`Failed to disassociate one or more roles.`)} - - - - ); -} - -export default withI18n()(TeamUsersList); diff --git a/awx/ui_next/src/screens/Team/TeamUsers/TeamUsersList.test.jsx b/awx/ui_next/src/screens/Team/TeamUsers/TeamUsersList.test.jsx deleted file mode 100644 index 2d393f8bf7..0000000000 --- a/awx/ui_next/src/screens/Team/TeamUsers/TeamUsersList.test.jsx +++ /dev/null @@ -1,261 +0,0 @@ -import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { TeamsAPI, UsersAPI } from '../../../api'; -import { - mountWithContexts, - waitForElement, -} from '../../../../testUtils/enzymeHelpers'; - -import TeamUsersList from './TeamUsersList'; - -jest.mock('../../../api/models/Teams'); -jest.mock('../../../api/models/Users'); - -const teamUsersList = { - data: { - count: 3, - results: [ - { - id: 1, - type: 'user', - url: '', - summary_fields: { - direct_access: [], - indirect_access: [ - { - role: { - id: 1, - }, - }, - ], - }, - created: '2020-06-19T12:55:13.138692Z', - username: 'admin', - first_name: '', - last_name: '', - email: 'a@g.com', - }, - { - id: 5, - type: 'user', - url: '', - summary_fields: { - direct_access: [ - { - role: { - id: 40, - name: 'Member', - user_capabilities: { - unattach: true, - }, - }, - descendant_roles: ['member_role', 'read_role'], - }, - { - role: { - id: 41, - name: 'Read', - user_capabilities: { - unattach: true, - }, - }, - descendant_roles: ['member_role', 'read_role'], - }, - ], - indirect_access: [], - }, - created: '2020-06-19T13:01:44.183577Z', - username: 'jt_admin', - first_name: '', - last_name: '', - email: '', - }, - { - id: 2, - type: 'user', - url: '', - summary_fields: { - direct_access: [ - { - role: { - id: 40, - name: 'Alex', - user_capabilities: { - unattach: true, - }, - }, - descendant_roles: ['member_role', 'read_role'], - }, - { - role: { - id: 41, - name: 'Read', - user_capabilities: { - unattach: true, - }, - }, - descendant_roles: ['member_role', 'read_role'], - }, - ], - indirect_access: [ - { - role: { - id: 2, - name: 'Admin', - user_capabilities: { - unattach: true, - }, - }, - descendant_roles: ['admin_role', 'member_role', 'read_role'], - }, - ], - }, - created: '2020-06-19T13:01:43.674349Z', - username: 'org_admin', - first_name: '', - last_name: '', - email: '', - }, - { - id: 3, - type: 'user', - url: '', - summary_fields: { - direct_access: [ - { - role: { - id: 40, - name: 'Savannah', - user_capabilities: { - unattach: true, - }, - }, - descendant_roles: ['member_role', 'read_role'], - }, - { - role: { - id: 41, - name: 'Read', - user_capabilities: { - unattach: true, - }, - }, - descendant_roles: ['member_role', 'read_role'], - }, - ], - indirect_access: [], - }, - created: '2020-06-19T13:01:43.868499Z', - username: 'org_member', - first_name: '', - last_name: '', - email: '', - }, - ], - }, -}; - -describe('', () => { - let wrapper; - - beforeEach(() => { - TeamsAPI.readUsersAccess = jest.fn(() => - Promise.resolve({ - data: teamUsersList.data, - }) - ); - TeamsAPI.readUsersAccessOptions = jest.fn(() => - Promise.resolve({ - data: { - actions: { - GET: {}, - POST: {}, - }, - }, - }) - ); - }); - afterEach(() => { - wrapper.unmount(); - jest.clearAllMocks(); - }); - - test('should load and render users', async () => { - await act(async () => { - wrapper = mountWithContexts(); - }); - wrapper.update(); - - expect(wrapper.find('TeamUserListItem')).toHaveLength(4); - }); - - test('should disassociate role', async () => { - await act(async () => { - wrapper = mountWithContexts(); - }); - wrapper.update(); - - await act(async () => { - wrapper.find('Label[aria-label="Member"]').prop('onClose')({ - id: 1, - name: 'Member', - }); - }); - wrapper.update(); - expect(wrapper.find('AlertModal[title="Disassociate roles"]').length).toBe( - 1 - ); - await act(async () => { - wrapper - .find('Button[aria-label="confirm disassociation"]') - .prop('onClick')(); - }); - - expect(UsersAPI.disassociateRole).toHaveBeenCalledTimes(1); - expect(TeamsAPI.readUsersAccess).toHaveBeenCalledTimes(2); - }); - - test('should show disassociation error', async () => { - UsersAPI.disassociateRole.mockResolvedValue( - new Error({ - response: { - config: { - method: 'post', - url: '/api/v2/users/1', - }, - data: 'An error occurred', - }, - }) - ); - - await act(async () => { - wrapper = mountWithContexts(); - }); - waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - - await act(async () => { - wrapper.find('Label[aria-label="Member"]').prop('onClose')({ - id: 1, - name: 'Member', - }); - }); - - wrapper.update(); - expect(wrapper.find('AlertModal[title="Disassociate roles"]').length).toBe( - 1 - ); - - await act(async () => { - wrapper - .find('Button[aria-label="confirm disassociation"]') - .prop('onClick')(); - }); - - wrapper.update(); - expect(UsersAPI.disassociateRole).toHaveBeenCalled(); - - const modal = wrapper.find('Modal'); - expect(modal).toHaveLength(1); - expect(modal.prop('title')).toEqual('Error!'); - }); -}); diff --git a/awx/ui_next/src/screens/Team/TeamUsers/index.js b/awx/ui_next/src/screens/Team/TeamUsers/index.js deleted file mode 100644 index 68714e00a2..0000000000 --- a/awx/ui_next/src/screens/Team/TeamUsers/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export { - default -} -from './TeamUsersList'