mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 10:30:03 -03:30
Merge pull request #7772 from mabashian/convert-ResourceAccessList-functional
Converts ResourceAccessList to functional component Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
commit
91d3f954cd
@ -1,19 +1,14 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
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 AddResourceRole from '../AddRole/AddResourceRole';
|
||||
import AlertModal from '../AlertModal';
|
||||
import DataListToolbar from '../DataListToolbar';
|
||||
import PaginatedDataList, { ToolbarAddButton } from '../PaginatedDataList';
|
||||
import {
|
||||
getQSConfig,
|
||||
encodeQueryString,
|
||||
parseQueryString,
|
||||
} from '../../util/qs';
|
||||
|
||||
import { getQSConfig, parseQueryString } from '../../util/qs';
|
||||
import useRequest, { useDeleteItems } from '../../util/useRequest';
|
||||
import DeleteRoleConfirmationModal from './DeleteRoleConfirmationModal';
|
||||
import ResourceAccessListItem from './ResourceAccessListItem';
|
||||
|
||||
@ -23,227 +18,160 @@ const QS_CONFIG = getQSConfig('access', {
|
||||
order_by: 'first_name',
|
||||
});
|
||||
|
||||
class ResourceAccessList extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
function ResourceAccessList({ i18n, apiModel, resource }) {
|
||||
const [deletionRecord, setDeletionRecord] = useState(null);
|
||||
const [deletionRole, setDeletionRole] = useState(null);
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const location = useLocation();
|
||||
|
||||
const {
|
||||
result: { accessRecords, itemCount },
|
||||
error: contentError,
|
||||
isLoading,
|
||||
request: fetchAccessRecords,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const params = parseQueryString(QS_CONFIG, location.search);
|
||||
const response = await apiModel.readAccessList(resource.id, params);
|
||||
return {
|
||||
accessRecords: response.data.results,
|
||||
itemCount: response.data.count,
|
||||
};
|
||||
}, [apiModel, location, resource.id]),
|
||||
{
|
||||
accessRecords: [],
|
||||
contentError: null,
|
||||
hasContentLoading: true,
|
||||
hasDeletionError: false,
|
||||
deletionRecord: null,
|
||||
deletionRole: null,
|
||||
isAddModalOpen: false,
|
||||
itemCount: 0,
|
||||
};
|
||||
this.loadAccessList = this.loadAccessList.bind(this);
|
||||
this.handleAddClose = this.handleAddClose.bind(this);
|
||||
this.handleAddOpen = this.handleAddOpen.bind(this);
|
||||
this.handleAddSuccess = this.handleAddSuccess.bind(this);
|
||||
this.handleDeleteCancel = this.handleDeleteCancel.bind(this);
|
||||
this.handleDeleteConfirm = this.handleDeleteConfirm.bind(this);
|
||||
this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this);
|
||||
this.handleDeleteOpen = this.handleDeleteOpen.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadAccessList();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { location } = this.props;
|
||||
|
||||
const prevParams = parseQueryString(QS_CONFIG, prevProps.location.search);
|
||||
const currentParams = parseQueryString(QS_CONFIG, location.search);
|
||||
|
||||
if (encodeQueryString(currentParams) !== encodeQueryString(prevParams)) {
|
||||
this.loadAccessList();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
async loadAccessList() {
|
||||
const { apiModel, resource, location } = this.props;
|
||||
const params = parseQueryString(QS_CONFIG, location.search);
|
||||
useEffect(() => {
|
||||
fetchAccessRecords();
|
||||
}, [fetchAccessRecords]);
|
||||
|
||||
this.setState({ contentError: null, hasContentLoading: true });
|
||||
try {
|
||||
const {
|
||||
data: { results: accessRecords = [], count: itemCount = 0 },
|
||||
} = await apiModel.readAccessList(resource.id, params);
|
||||
this.setState({ itemCount, accessRecords });
|
||||
} catch (err) {
|
||||
this.setState({ contentError: err });
|
||||
} finally {
|
||||
this.setState({ hasContentLoading: false });
|
||||
const {
|
||||
isLoading: isDeleteLoading,
|
||||
deleteItems: deleteRole,
|
||||
deletionError,
|
||||
clearDeletionError,
|
||||
} = useDeleteItems(
|
||||
useCallback(async () => {
|
||||
if (typeof deletionRole.team_id !== 'undefined') {
|
||||
return TeamsAPI.disassociateRole(deletionRole.team_id, deletionRole.id);
|
||||
}
|
||||
return UsersAPI.disassociateRole(deletionRecord.id, deletionRole.id);
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
}, [deletionRole]),
|
||||
{
|
||||
qsConfig: QS_CONFIG,
|
||||
fetchItems: fetchAccessRecords,
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
handleDeleteOpen(deletionRole, deletionRecord) {
|
||||
this.setState({ deletionRole, deletionRecord });
|
||||
}
|
||||
|
||||
handleDeleteCancel() {
|
||||
this.setState({ deletionRole: null, deletionRecord: null });
|
||||
}
|
||||
|
||||
handleDeleteErrorClose() {
|
||||
this.setState({
|
||||
hasDeletionError: false,
|
||||
deletionRecord: null,
|
||||
deletionRole: null,
|
||||
});
|
||||
}
|
||||
|
||||
async handleDeleteConfirm() {
|
||||
const { deletionRole, deletionRecord } = this.state;
|
||||
|
||||
if (!deletionRole || !deletionRecord) {
|
||||
return;
|
||||
}
|
||||
|
||||
let promise;
|
||||
if (typeof deletionRole.team_id !== 'undefined') {
|
||||
promise = TeamsAPI.disassociateRole(
|
||||
deletionRole.team_id,
|
||||
deletionRole.id
|
||||
);
|
||||
} else {
|
||||
promise = UsersAPI.disassociateRole(deletionRecord.id, deletionRole.id);
|
||||
}
|
||||
|
||||
this.setState({ hasContentLoading: true });
|
||||
try {
|
||||
await promise.then(this.loadAccessList);
|
||||
this.setState({
|
||||
deletionRole: null,
|
||||
deletionRecord: null,
|
||||
});
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
hasContentLoading: false,
|
||||
hasDeletionError: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleAddClose() {
|
||||
this.setState({ isAddModalOpen: false });
|
||||
}
|
||||
|
||||
handleAddOpen() {
|
||||
this.setState({ isAddModalOpen: true });
|
||||
}
|
||||
|
||||
handleAddSuccess() {
|
||||
this.setState({ isAddModalOpen: false });
|
||||
this.loadAccessList();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { resource, i18n } = this.props;
|
||||
const {
|
||||
accessRecords,
|
||||
contentError,
|
||||
hasContentLoading,
|
||||
deletionRole,
|
||||
deletionRecord,
|
||||
hasDeletionError,
|
||||
itemCount,
|
||||
isAddModalOpen,
|
||||
} = this.state;
|
||||
const canEdit = resource.summary_fields.user_capabilities.edit;
|
||||
const isDeleteModalOpen =
|
||||
!hasContentLoading && !hasDeletionError && deletionRole;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<PaginatedDataList
|
||||
error={contentError}
|
||||
hasContentLoading={hasContentLoading}
|
||||
items={accessRecords}
|
||||
itemCount={itemCount}
|
||||
pluralizedItemName={i18n._(t`Roles`)}
|
||||
qsConfig={QS_CONFIG}
|
||||
toolbarSearchColumns={[
|
||||
{
|
||||
name: i18n._(t`Username`),
|
||||
key: 'username',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`First Name`),
|
||||
key: 'first_name',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Last Name`),
|
||||
key: 'last_name',
|
||||
},
|
||||
]}
|
||||
toolbarSortColumns={[
|
||||
{
|
||||
name: i18n._(t`Username`),
|
||||
key: 'username',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`First Name`),
|
||||
key: 'first_name',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Last Name`),
|
||||
key: 'last_name',
|
||||
},
|
||||
]}
|
||||
renderToolbar={props => (
|
||||
<DataListToolbar
|
||||
{...props}
|
||||
qsConfig={QS_CONFIG}
|
||||
additionalControls={
|
||||
canEdit
|
||||
? [
|
||||
<ToolbarAddButton
|
||||
key="add"
|
||||
onClick={this.handleAddOpen}
|
||||
/>,
|
||||
]
|
||||
: []
|
||||
}
|
||||
/>
|
||||
)}
|
||||
renderItem={accessRecord => (
|
||||
<ResourceAccessListItem
|
||||
key={accessRecord.id}
|
||||
accessRecord={accessRecord}
|
||||
onRoleDelete={this.handleDeleteOpen}
|
||||
/>
|
||||
)}
|
||||
return (
|
||||
<>
|
||||
<PaginatedDataList
|
||||
error={contentError}
|
||||
hasContentLoading={isLoading || isDeleteLoading}
|
||||
items={accessRecords}
|
||||
itemCount={itemCount}
|
||||
pluralizedItemName={i18n._(t`Roles`)}
|
||||
qsConfig={QS_CONFIG}
|
||||
toolbarSearchColumns={[
|
||||
{
|
||||
name: i18n._(t`Username`),
|
||||
key: 'username',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`First name`),
|
||||
key: 'first_name',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Last name`),
|
||||
key: 'last_name',
|
||||
},
|
||||
]}
|
||||
toolbarSortColumns={[
|
||||
{
|
||||
name: i18n._(t`Username`),
|
||||
key: 'username',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`First name`),
|
||||
key: 'first_name',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Last name`),
|
||||
key: 'last_name',
|
||||
},
|
||||
]}
|
||||
renderToolbar={props => (
|
||||
<DataListToolbar
|
||||
{...props}
|
||||
qsConfig={QS_CONFIG}
|
||||
additionalControls={
|
||||
resource?.summary_fields?.user_capabilities?.edit
|
||||
? [
|
||||
<ToolbarAddButton
|
||||
key="add"
|
||||
onClick={() => setShowAddModal(true)}
|
||||
/>,
|
||||
]
|
||||
: []
|
||||
}
|
||||
/>
|
||||
)}
|
||||
renderItem={accessRecord => (
|
||||
<ResourceAccessListItem
|
||||
key={accessRecord.id}
|
||||
accessRecord={accessRecord}
|
||||
onRoleDelete={(role, record) => {
|
||||
setDeletionRecord(record);
|
||||
setDeletionRole(role);
|
||||
setShowDeleteModal(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{showAddModal && (
|
||||
<AddResourceRole
|
||||
onClose={() => setShowAddModal(false)}
|
||||
onSave={() => {
|
||||
setShowAddModal(false);
|
||||
fetchAccessRecords();
|
||||
}}
|
||||
roles={resource.summary_fields.object_roles}
|
||||
/>
|
||||
{isAddModalOpen && (
|
||||
<AddResourceRole
|
||||
onClose={this.handleAddClose}
|
||||
onSave={this.handleAddSuccess}
|
||||
roles={resource.summary_fields.object_roles}
|
||||
/>
|
||||
)}
|
||||
{isDeleteModalOpen && (
|
||||
<DeleteRoleConfirmationModal
|
||||
role={deletionRole}
|
||||
username={deletionRecord.username}
|
||||
onCancel={this.handleDeleteCancel}
|
||||
onConfirm={this.handleDeleteConfirm}
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
{showDeleteModal && (
|
||||
<DeleteRoleConfirmationModal
|
||||
role={deletionRole}
|
||||
username={deletionRecord.username}
|
||||
onCancel={() => {
|
||||
setDeletionRecord(null);
|
||||
setDeletionRole(null);
|
||||
setShowDeleteModal(false);
|
||||
}}
|
||||
onConfirm={async () => {
|
||||
await deleteRole();
|
||||
setShowDeleteModal(false);
|
||||
setDeletionRecord(null);
|
||||
setDeletionRole(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{deletionError && (
|
||||
<AlertModal
|
||||
isOpen={hasDeletionError}
|
||||
isOpen={deletionError}
|
||||
variant="error"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={this.handleDeleteErrorClose}
|
||||
onClose={clearDeletionError}
|
||||
>
|
||||
{i18n._(t`Failed to delete role`)}
|
||||
</AlertModal>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { ResourceAccessList as _ResourceAccessList };
|
||||
export default withI18n()(withRouter(ResourceAccessList));
|
||||
export default withI18n()(ResourceAccessList);
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
import { sleep } from '../../../testUtils/testUtils';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
@ -13,6 +12,7 @@ import ResourceAccessList from './ResourceAccessList';
|
||||
jest.mock('../../api');
|
||||
|
||||
describe('<ResourceAccessList />', () => {
|
||||
let wrapper;
|
||||
const organization = {
|
||||
id: 1,
|
||||
name: 'Default',
|
||||
@ -74,108 +74,68 @@ describe('<ResourceAccessList />', () => {
|
||||
],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
OrganizationsAPI.readAccessList.mockResolvedValue({ data });
|
||||
TeamsAPI.disassociateRole.mockResolvedValue({});
|
||||
UsersAPI.disassociateRole.mockResolvedValue({});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ResourceAccessList
|
||||
resource={organization}
|
||||
apiModel={OrganizationsAPI}
|
||||
/>
|
||||
);
|
||||
});
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('initially renders succesfully', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<ResourceAccessList resource={organization} apiModel={OrganizationsAPI} />
|
||||
);
|
||||
expect(wrapper.find('PaginatedDataList')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should fetch and display access records on mount', async done => {
|
||||
const wrapper = mountWithContexts(
|
||||
<ResourceAccessList resource={organization} apiModel={OrganizationsAPI} />
|
||||
);
|
||||
await waitForElement(
|
||||
wrapper,
|
||||
'ResourceAccessListItem',
|
||||
el => el.length === 2
|
||||
);
|
||||
expect(wrapper.find('PaginatedDataList').prop('items')).toEqual(
|
||||
data.results
|
||||
);
|
||||
expect(wrapper.find('ResourceAccessList').state('hasContentLoading')).toBe(
|
||||
false
|
||||
);
|
||||
expect(wrapper.find('ResourceAccessList').state('contentError')).toBe(null);
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
expect(OrganizationsAPI.readAccessList).toHaveBeenCalled();
|
||||
expect(wrapper.find('ResourceAccessListItem').length).toBe(2);
|
||||
done();
|
||||
});
|
||||
|
||||
test('should open confirmation dialog when deleting role', async done => {
|
||||
const wrapper = mountWithContexts(
|
||||
<ResourceAccessList resource={organization} apiModel={OrganizationsAPI} />
|
||||
);
|
||||
await sleep(0);
|
||||
wrapper.update();
|
||||
|
||||
test('should open and close confirmation dialog when deleting role', async done => {
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
expect(wrapper.find('DeleteRoleConfirmationModal')).toHaveLength(0);
|
||||
const button = wrapper.find('Chip Button').at(0);
|
||||
button.prop('onClick')();
|
||||
await act(async () => {
|
||||
button.prop('onClick')();
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
const component = wrapper.find('ResourceAccessList');
|
||||
expect(component.state('deletionRole')).toEqual(
|
||||
data.results[0].summary_fields.direct_access[0].role
|
||||
);
|
||||
expect(component.state('deletionRecord')).toEqual(data.results[0]);
|
||||
expect(component.find('DeleteRoleConfirmationModal')).toHaveLength(1);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should close dialog when cancel button clicked', async done => {
|
||||
const wrapper = mountWithContexts(
|
||||
<ResourceAccessList resource={organization} apiModel={OrganizationsAPI} />
|
||||
);
|
||||
await sleep(0);
|
||||
expect(wrapper.find('DeleteRoleConfirmationModal')).toHaveLength(1);
|
||||
await act(async () => {
|
||||
wrapper.find('DeleteRoleConfirmationModal').prop('onCancel')();
|
||||
});
|
||||
wrapper.update();
|
||||
const button = wrapper.find('Chip Button').at(0);
|
||||
button.prop('onClick')();
|
||||
wrapper.update();
|
||||
|
||||
wrapper.find('DeleteRoleConfirmationModal').prop('onCancel')();
|
||||
const component = wrapper.find('ResourceAccessList');
|
||||
expect(component.state('deletionRole')).toBeNull();
|
||||
expect(component.state('deletionRecord')).toBeNull();
|
||||
expect(wrapper.find('DeleteRoleConfirmationModal')).toHaveLength(0);
|
||||
expect(TeamsAPI.disassociateRole).not.toHaveBeenCalled();
|
||||
expect(UsersAPI.disassociateRole).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
it('should delete user role', async done => {
|
||||
const wrapper = mountWithContexts(
|
||||
<ResourceAccessList resource={organization} apiModel={OrganizationsAPI} />
|
||||
);
|
||||
const button = await waitForElement(
|
||||
wrapper,
|
||||
'Chip Button',
|
||||
el => el.length === 2
|
||||
);
|
||||
button.at(0).prop('onClick')();
|
||||
|
||||
const confirmation = await waitForElement(
|
||||
wrapper,
|
||||
'DeleteRoleConfirmationModal'
|
||||
);
|
||||
confirmation.prop('onConfirm')();
|
||||
await waitForElement(
|
||||
wrapper,
|
||||
'DeleteRoleConfirmationModal',
|
||||
el => el.length === 0
|
||||
);
|
||||
|
||||
await sleep(0);
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
const button = wrapper.find('Chip Button').at(0);
|
||||
await act(async () => {
|
||||
button.prop('onClick')();
|
||||
});
|
||||
wrapper.update();
|
||||
const component = wrapper.find('ResourceAccessList');
|
||||
expect(component.state('deletionRole')).toBeNull();
|
||||
expect(component.state('deletionRecord')).toBeNull();
|
||||
await act(async () => {
|
||||
wrapper.find('DeleteRoleConfirmationModal').prop('onConfirm')();
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('DeleteRoleConfirmationModal')).toHaveLength(0);
|
||||
expect(TeamsAPI.disassociateRole).not.toHaveBeenCalled();
|
||||
expect(UsersAPI.disassociateRole).toHaveBeenCalledWith(1, 1);
|
||||
expect(OrganizationsAPI.readAccessList).toHaveBeenCalledTimes(2);
|
||||
@ -183,32 +143,17 @@ describe('<ResourceAccessList />', () => {
|
||||
});
|
||||
|
||||
it('should delete team role', async done => {
|
||||
const wrapper = mountWithContexts(
|
||||
<ResourceAccessList resource={organization} apiModel={OrganizationsAPI} />
|
||||
);
|
||||
const button = await waitForElement(
|
||||
wrapper,
|
||||
'Chip Button',
|
||||
el => el.length === 2
|
||||
);
|
||||
button.at(1).prop('onClick')();
|
||||
|
||||
const confirmation = await waitForElement(
|
||||
wrapper,
|
||||
'DeleteRoleConfirmationModal'
|
||||
);
|
||||
confirmation.prop('onConfirm')();
|
||||
await waitForElement(
|
||||
wrapper,
|
||||
'DeleteRoleConfirmationModal',
|
||||
el => el.length === 0
|
||||
);
|
||||
|
||||
await sleep(0);
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
const button = wrapper.find('Chip Button').at(1);
|
||||
await act(async () => {
|
||||
button.prop('onClick')();
|
||||
});
|
||||
wrapper.update();
|
||||
const component = wrapper.find('ResourceAccessList');
|
||||
expect(component.state('deletionRole')).toBeNull();
|
||||
expect(component.state('deletionRecord')).toBeNull();
|
||||
await act(async () => {
|
||||
wrapper.find('DeleteRoleConfirmationModal').prop('onConfirm')();
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('DeleteRoleConfirmationModal')).toHaveLength(0);
|
||||
expect(TeamsAPI.disassociateRole).toHaveBeenCalledWith(5, 3);
|
||||
expect(UsersAPI.disassociateRole).not.toHaveBeenCalled();
|
||||
expect(OrganizationsAPI.readAccessList).toHaveBeenCalledTimes(2);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user