mirror of
https://github.com/ansible/awx.git
synced 2026-04-10 12:39:22 -02:30
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:
@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react';
|
|||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { TeamsAPI, UsersAPI } from '../../api';
|
import { RolesAPI, TeamsAPI, UsersAPI } from '../../api';
|
||||||
import AddResourceRole from '../AddRole/AddResourceRole';
|
import AddResourceRole from '../AddRole/AddResourceRole';
|
||||||
import AlertModal from '../AlertModal';
|
import AlertModal from '../AlertModal';
|
||||||
import DataListToolbar from '../DataListToolbar';
|
import DataListToolbar from '../DataListToolbar';
|
||||||
@@ -26,7 +26,13 @@ function ResourceAccessList({ i18n, apiModel, resource }) {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: { accessRecords, itemCount, relatedSearchableKeys, searchableKeys },
|
result: {
|
||||||
|
accessRecords,
|
||||||
|
itemCount,
|
||||||
|
relatedSearchableKeys,
|
||||||
|
searchableKeys,
|
||||||
|
organizationRoles,
|
||||||
|
},
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading,
|
isLoading,
|
||||||
request: fetchAccessRecords,
|
request: fetchAccessRecords,
|
||||||
@@ -37,6 +43,41 @@ function ResourceAccessList({ i18n, apiModel, resource }) {
|
|||||||
apiModel.readAccessList(resource.id, params),
|
apiModel.readAccessList(resource.id, params),
|
||||||
apiModel.readAccessOptions(resource.id),
|
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 {
|
return {
|
||||||
accessRecords: response.data.results,
|
accessRecords: response.data.results,
|
||||||
itemCount: response.data.count,
|
itemCount: response.data.count,
|
||||||
@@ -46,8 +87,9 @@ function ResourceAccessList({ i18n, apiModel, resource }) {
|
|||||||
searchableKeys: Object.keys(
|
searchableKeys: Object.keys(
|
||||||
actionsResponse.data.actions?.GET || {}
|
actionsResponse.data.actions?.GET || {}
|
||||||
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
|
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
|
||||||
|
organizationRoles: orgRoles,
|
||||||
};
|
};
|
||||||
}, [apiModel, location, resource.id]),
|
}, [apiModel, location, resource]),
|
||||||
{
|
{
|
||||||
accessRecords: [],
|
accessRecords: [],
|
||||||
itemCount: 0,
|
itemCount: 0,
|
||||||
@@ -78,6 +120,29 @@ function ResourceAccessList({ i18n, apiModel, resource }) {
|
|||||||
fetchItems: fetchAccessRecords,
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -88,21 +153,7 @@ function ResourceAccessList({ i18n, apiModel, resource }) {
|
|||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
pluralizedItemName={i18n._(t`Roles`)}
|
pluralizedItemName={i18n._(t`Roles`)}
|
||||||
qsConfig={QS_CONFIG}
|
qsConfig={QS_CONFIG}
|
||||||
toolbarSearchColumns={[
|
toolbarSearchColumns={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',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
toolbarSortColumns={[
|
toolbarSortColumns={[
|
||||||
{
|
{
|
||||||
name: i18n._(t`Username`),
|
name: i18n._(t`Username`),
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
import {
|
import {
|
||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../testUtils/enzymeHelpers';
|
} from '../../../testUtils/enzymeHelpers';
|
||||||
|
|
||||||
import { OrganizationsAPI, TeamsAPI, UsersAPI } from '../../api';
|
import { OrganizationsAPI, TeamsAPI, UsersAPI, RolesAPI } from '../../api';
|
||||||
|
|
||||||
import ResourceAccessList from './ResourceAccessList';
|
import ResourceAccessList from './ResourceAccessList';
|
||||||
|
|
||||||
@@ -17,7 +18,24 @@ describe('<ResourceAccessList />', () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
name: 'Default',
|
name: 'Default',
|
||||||
summary_fields: {
|
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: {
|
user_capabilities: {
|
||||||
edit: true,
|
edit: true,
|
||||||
},
|
},
|
||||||
@@ -87,12 +105,24 @@ describe('<ResourceAccessList />', () => {
|
|||||||
});
|
});
|
||||||
TeamsAPI.disassociateRole.mockResolvedValue({});
|
TeamsAPI.disassociateRole.mockResolvedValue({});
|
||||||
UsersAPI.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 () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<ResourceAccessList
|
<ResourceAccessList
|
||||||
resource={organization}
|
resource={organization}
|
||||||
apiModel={OrganizationsAPI}
|
apiModel={OrganizationsAPI}
|
||||||
/>
|
/>,
|
||||||
|
{ context: { router: { history } } }
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
@@ -168,4 +198,24 @@ describe('<ResourceAccessList />', () => {
|
|||||||
expect(OrganizationsAPI.readAccessList).toHaveBeenCalledTimes(2);
|
expect(OrganizationsAPI.readAccessList).toHaveBeenCalledTimes(2);
|
||||||
done();
|
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'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ function ResourceAccessListItem({ accessRecord, onRoleDelete, i18n }) {
|
|||||||
onRoleDelete(role, accessRecord);
|
onRoleDelete(role, accessRecord);
|
||||||
}}
|
}}
|
||||||
isReadOnly={!role.user_capabilities.unattach}
|
isReadOnly={!role.user_capabilities.unattach}
|
||||||
|
ouiaId={`${role.name}-${role.id}`}
|
||||||
|
closeBtnAriaLabel={i18n._(t`Remove ${role.name} chip`)}
|
||||||
>
|
>
|
||||||
{role.name}
|
{role.name}
|
||||||
</Chip>
|
</Chip>
|
||||||
|
|||||||
@@ -98,11 +98,12 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
|||||||
>
|
>
|
||||||
<Chip
|
<Chip
|
||||||
className=""
|
className=""
|
||||||
closeBtnAriaLabel="close"
|
closeBtnAriaLabel="Remove Member chip"
|
||||||
component="div"
|
component="div"
|
||||||
isOverflowChip={false}
|
isOverflowChip={false}
|
||||||
isReadOnly={false}
|
isReadOnly={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
ouiaId="Member-3"
|
||||||
tooltipPosition="top"
|
tooltipPosition="top"
|
||||||
>
|
>
|
||||||
Member
|
Member
|
||||||
@@ -164,11 +165,12 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
|||||||
>
|
>
|
||||||
<Chip
|
<Chip
|
||||||
className=""
|
className=""
|
||||||
closeBtnAriaLabel="close"
|
closeBtnAriaLabel="Remove Member chip"
|
||||||
component="div"
|
component="div"
|
||||||
isOverflowChip={false}
|
isOverflowChip={false}
|
||||||
isReadOnly={false}
|
isReadOnly={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
ouiaId="Member-3"
|
||||||
tooltipPosition="top"
|
tooltipPosition="top"
|
||||||
>
|
>
|
||||||
Member
|
Member
|
||||||
@@ -253,11 +255,12 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
|||||||
>
|
>
|
||||||
<Chip
|
<Chip
|
||||||
className=""
|
className=""
|
||||||
closeBtnAriaLabel="close"
|
closeBtnAriaLabel="Remove Member chip"
|
||||||
component="div"
|
component="div"
|
||||||
isOverflowChip={false}
|
isOverflowChip={false}
|
||||||
isReadOnly={false}
|
isReadOnly={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
ouiaId="Member-3"
|
||||||
tooltipPosition="top"
|
tooltipPosition="top"
|
||||||
>
|
>
|
||||||
Member
|
Member
|
||||||
@@ -688,11 +691,12 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
|||||||
>
|
>
|
||||||
<Chip
|
<Chip
|
||||||
className=""
|
className=""
|
||||||
closeBtnAriaLabel="close"
|
closeBtnAriaLabel="Remove Member chip"
|
||||||
component="div"
|
component="div"
|
||||||
isOverflowChip={false}
|
isOverflowChip={false}
|
||||||
isReadOnly={false}
|
isReadOnly={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
ouiaId="Member-3"
|
||||||
tooltipPosition="top"
|
tooltipPosition="top"
|
||||||
>
|
>
|
||||||
Member
|
Member
|
||||||
@@ -865,12 +869,13 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
|||||||
>
|
>
|
||||||
<Chip
|
<Chip
|
||||||
className=""
|
className=""
|
||||||
closeBtnAriaLabel="close"
|
closeBtnAriaLabel="Remove Member chip"
|
||||||
component="div"
|
component="div"
|
||||||
isOverflowChip={false}
|
isOverflowChip={false}
|
||||||
isReadOnly={false}
|
isReadOnly={false}
|
||||||
key=".$3"
|
key=".$3"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
ouiaId="Member-3"
|
||||||
tooltipPosition="top"
|
tooltipPosition="top"
|
||||||
>
|
>
|
||||||
<GenerateId
|
<GenerateId
|
||||||
@@ -878,7 +883,7 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="pf-c-chip"
|
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-component-type="PF4/Chip"
|
||||||
data-ouia-safe={true}
|
data-ouia-safe={true}
|
||||||
>
|
>
|
||||||
@@ -889,19 +894,19 @@ exports[`<ResourceAccessListItem /> initially renders succesfully 1`] = `
|
|||||||
Member
|
Member
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
aria-label="close"
|
aria-label="Remove Member chip"
|
||||||
aria-labelledby="remove_pf-random-id-1 pf-random-id-1"
|
aria-labelledby="remove_pf-random-id-1 pf-random-id-1"
|
||||||
id="remove_pf-random-id-1"
|
id="remove_pf-random-id-1"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
ouiaId="close"
|
ouiaId="Member-3"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-disabled={false}
|
aria-disabled={false}
|
||||||
aria-label="close"
|
aria-label="Remove Member chip"
|
||||||
aria-labelledby="remove_pf-random-id-1 pf-random-id-1"
|
aria-labelledby="remove_pf-random-id-1 pf-random-id-1"
|
||||||
className="pf-c-button pf-m-plain"
|
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-component-type="PF4/Button"
|
||||||
data-ouia-safe={true}
|
data-ouia-safe={true}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
|
|||||||
Reference in New Issue
Block a user