Merge pull request #8640 from AlexSCorey/8130-DiplayOrgAdmins

Adds filter by role on Org access lists

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot] 2021-02-17 21:00:09 +00:00 committed by GitHub
commit c98c6db664
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 139 additions and 31 deletions

View File

@ -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`),

View File

@ -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('<ResourceAccessList />', () => {
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('<ResourceAccessList />', () => {
});
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(
<ResourceAccessList
resource={organization}
apiModel={OrganizationsAPI}
/>
/>,
{ context: { router: { history } } }
);
});
wrapper.update();
@ -168,4 +198,24 @@ describe('<ResourceAccessList />', () => {
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'],
],
},
]);
});
});

View File

@ -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}
</Chip>

View File

@ -98,11 +98,12 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
>
<Chip
className=""
closeBtnAriaLabel="close"
closeBtnAriaLabel="Remove Member chip"
component="div"
isOverflowChip={false}
isReadOnly={false}
onClick={[Function]}
ouiaId="Member-3"
tooltipPosition="top"
>
Member
@ -164,11 +165,12 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
>
<Chip
className=""
closeBtnAriaLabel="close"
closeBtnAriaLabel="Remove Member chip"
component="div"
isOverflowChip={false}
isReadOnly={false}
onClick={[Function]}
ouiaId="Member-3"
tooltipPosition="top"
>
Member
@ -253,11 +255,12 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
>
<Chip
className=""
closeBtnAriaLabel="close"
closeBtnAriaLabel="Remove Member chip"
component="div"
isOverflowChip={false}
isReadOnly={false}
onClick={[Function]}
ouiaId="Member-3"
tooltipPosition="top"
>
Member
@ -688,11 +691,12 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
>
<Chip
className=""
closeBtnAriaLabel="close"
closeBtnAriaLabel="Remove Member chip"
component="div"
isOverflowChip={false}
isReadOnly={false}
onClick={[Function]}
ouiaId="Member-3"
tooltipPosition="top"
>
Member
@ -865,12 +869,13 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
>
<Chip
className=""
closeBtnAriaLabel="close"
closeBtnAriaLabel="Remove Member chip"
component="div"
isOverflowChip={false}
isReadOnly={false}
key=".$3"
onClick={[Function]}
ouiaId="Member-3"
tooltipPosition="top"
>
<GenerateId
@ -878,7 +883,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
>
<div
className="pf-c-chip"
data-ouia-component-id="OUIA-Generated-Chip-1"
data-ouia-component-id="Member-3"
data-ouia-component-type="PF4/Chip"
data-ouia-safe={true}
>
@ -889,19 +894,19 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
Member
</span>
<Button
aria-label="close"
aria-label="Remove Member chip"
aria-labelledby="remove_pf-random-id-1 pf-random-id-1"
id="remove_pf-random-id-1"
onClick={[Function]}
ouiaId="close"
ouiaId="Member-3"
variant="plain"
>
<button
aria-disabled={false}
aria-label="close"
aria-label="Remove Member chip"
aria-labelledby="remove_pf-random-id-1 pf-random-id-1"
className="pf-c-button pf-m-plain"
data-ouia-component-id="close"
data-ouia-component-id="Member-3"
data-ouia-component-type="PF4/Button"
data-ouia-safe={true}
disabled={false}