diff --git a/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx b/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx index 0f5f7c1c64..1cc04c5fa3 100644 --- a/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx +++ b/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { TeamsAPI, UsersAPI } from '../../api'; +import { RolesAPI, TeamsAPI, UsersAPI } from '../../api'; import AddResourceRole from '../AddRole/AddResourceRole'; import AlertModal from '../AlertModal'; import DataListToolbar from '../DataListToolbar'; @@ -26,7 +26,13 @@ function ResourceAccessList({ i18n, apiModel, resource }) { const location = useLocation(); const { - result: { accessRecords, itemCount, relatedSearchableKeys, searchableKeys }, + result: { + accessRecords, + itemCount, + relatedSearchableKeys, + searchableKeys, + organizationRoles, + }, error: contentError, isLoading, request: fetchAccessRecords, @@ -37,6 +43,41 @@ function ResourceAccessList({ i18n, apiModel, resource }) { apiModel.readAccessList(resource.id, params), apiModel.readAccessOptions(resource.id), ]); + + // Eventually this could be expanded to other access lists. + // We will need to combine the role ids of all the different level + // of resource level roles. + + let orgRoles; + if (location.pathname.includes('/organizations')) { + const { + data: { results: roles }, + } = await RolesAPI.read({ content_type__isnull: true }); + const sysAdmin = roles.filter( + role => role.name === 'System Administrator' + ); + const sysAud = roles.filter(role => { + let auditor; + if (role.name === 'System Auditor') { + auditor = role.id; + } + return auditor; + }); + + orgRoles = Object.values(resource.summary_fields.object_roles).map( + opt => { + let item; + if (opt.name === 'Admin') { + item = [`${opt.id}, ${sysAdmin[0].id}`, opt.name]; + } else if (sysAud[0].id && opt.name === 'Auditor') { + item = [`${sysAud[0].id}, ${opt.id}`, opt.name]; + } else { + item = [`${opt.id}`, opt.name]; + } + return item; + } + ); + } return { accessRecords: response.data.results, itemCount: response.data.count, @@ -46,8 +87,9 @@ function ResourceAccessList({ i18n, apiModel, resource }) { searchableKeys: Object.keys( actionsResponse.data.actions?.GET || {} ).filter(key => actionsResponse.data.actions?.GET[key].filterable), + organizationRoles: orgRoles, }; - }, [apiModel, location, resource.id]), + }, [apiModel, location, resource]), { accessRecords: [], itemCount: 0, @@ -78,6 +120,29 @@ function ResourceAccessList({ i18n, apiModel, resource }) { fetchItems: fetchAccessRecords, } ); + const toolbarSearchColumns = [ + { + name: i18n._(t`Username`), + key: 'username__icontains', + isDefault: true, + }, + { + name: i18n._(t`First Name`), + key: 'first_name__icontains', + }, + { + name: i18n._(t`Last Name`), + key: 'last_name__icontains', + }, + ]; + + if (organizationRoles?.length > 0) { + toolbarSearchColumns.push({ + name: i18n._(t`Roles`), + key: `or__roles__in`, + options: organizationRoles, + }); + } return ( <> @@ -88,21 +153,7 @@ function ResourceAccessList({ i18n, apiModel, resource }) { itemCount={itemCount} pluralizedItemName={i18n._(t`Roles`)} qsConfig={QS_CONFIG} - toolbarSearchColumns={[ - { - name: i18n._(t`Username`), - key: 'username__icontains', - isDefault: true, - }, - { - name: i18n._(t`First Name`), - key: 'first_name__icontains', - }, - { - name: i18n._(t`Last Name`), - key: 'last_name__icontains', - }, - ]} + toolbarSearchColumns={toolbarSearchColumns} toolbarSortColumns={[ { name: i18n._(t`Username`), diff --git a/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.test.jsx b/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.test.jsx index 98b6371414..c77a86f49d 100644 --- a/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.test.jsx +++ b/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.test.jsx @@ -1,11 +1,12 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; +import { createMemoryHistory } from 'history'; import { mountWithContexts, waitForElement, } from '../../../testUtils/enzymeHelpers'; -import { OrganizationsAPI, TeamsAPI, UsersAPI } from '../../api'; +import { OrganizationsAPI, TeamsAPI, UsersAPI, RolesAPI } from '../../api'; import ResourceAccessList from './ResourceAccessList'; @@ -17,7 +18,24 @@ describe('', () => { id: 1, name: 'Default', summary_fields: { - object_roles: {}, + object_roles: { + admin_role: { + description: 'Can manage all aspects of the organization', + name: 'Admin', + id: 2, + user_only: true, + }, + execute_role: { + description: 'May run any executable resources in the organization', + name: 'Execute', + id: 3, + }, + project_admin_role: { + description: 'Can manage all projects of the organization', + name: 'Project Admin', + id: 4, + }, + }, user_capabilities: { edit: true, }, @@ -87,12 +105,24 @@ describe('', () => { }); TeamsAPI.disassociateRole.mockResolvedValue({}); UsersAPI.disassociateRole.mockResolvedValue({}); + RolesAPI.read.mockResolvedValue({ + data: { + results: [ + { id: 1, name: 'System Administrator' }, + { id: 14, name: 'System Auditor' }, + ], + }, + }); + const history = createMemoryHistory({ + initialEntries: ['/organizations/1/access'], + }); await act(async () => { wrapper = mountWithContexts( + />, + { context: { router: { history } } } ); }); wrapper.update(); @@ -168,4 +198,24 @@ describe('', () => { expect(OrganizationsAPI.readAccessList).toHaveBeenCalledTimes(2); done(); }); + test('should call api to get org details', async () => { + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + + expect( + wrapper.find('PaginatedDataList').prop('toolbarSearchColumns') + ).toStrictEqual([ + { isDefault: true, key: 'username__icontains', name: 'Username' }, + { key: 'first_name__icontains', name: 'First Name' }, + { key: 'last_name__icontains', name: 'Last Name' }, + { + key: 'or__roles__in', + name: 'Roles', + options: [ + ['2, 1', 'Admin'], + ['3', 'Execute'], + ['4', 'Project Admin'], + ], + }, + ]); + }); }); diff --git a/awx/ui_next/src/components/ResourceAccessList/ResourceAccessListItem.jsx b/awx/ui_next/src/components/ResourceAccessList/ResourceAccessListItem.jsx index d641e67e01..669c5223ab 100644 --- a/awx/ui_next/src/components/ResourceAccessList/ResourceAccessListItem.jsx +++ b/awx/ui_next/src/components/ResourceAccessList/ResourceAccessListItem.jsx @@ -56,6 +56,8 @@ function ResourceAccessListItem({ accessRecord, onRoleDelete, i18n }) { onRoleDelete(role, accessRecord); }} isReadOnly={!role.user_capabilities.unattach} + ouiaId={`${role.name}-${role.id}`} + closeBtnAriaLabel={i18n._(t`Remove ${role.name} chip`)} > {role.name} diff --git a/awx/ui_next/src/components/ResourceAccessList/__snapshots__/ResourceAccessListItem.test.jsx.snap b/awx/ui_next/src/components/ResourceAccessList/__snapshots__/ResourceAccessListItem.test.jsx.snap index 7271c0a62b..7d1c5a4da8 100644 --- a/awx/ui_next/src/components/ResourceAccessList/__snapshots__/ResourceAccessListItem.test.jsx.snap +++ b/awx/ui_next/src/components/ResourceAccessList/__snapshots__/ResourceAccessListItem.test.jsx.snap @@ -98,11 +98,12 @@ exports[` initially renders succesfully 1`] = ` > Member @@ -164,11 +165,12 @@ exports[` initially renders succesfully 1`] = ` > Member @@ -253,11 +255,12 @@ exports[` initially renders succesfully 1`] = ` > Member @@ -688,11 +691,12 @@ exports[` initially renders succesfully 1`] = ` > Member @@ -865,12 +869,13 @@ exports[` initially renders succesfully 1`] = ` > initially renders succesfully 1`] = ` >
@@ -889,19 +894,19 @@ exports[` initially renders succesfully 1`] = ` Member