diff --git a/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx b/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx
index 10ac42b77f..6d8a1bf280 100644
--- a/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx
+++ b/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.jsx
@@ -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 (
-
- (
- ,
- ]
- : []
- }
- />
- )}
- renderItem={accessRecord => (
-
- )}
+ return (
+ <>
+ (
+ setShowAddModal(true)}
+ />,
+ ]
+ : []
+ }
+ />
+ )}
+ renderItem={accessRecord => (
+ {
+ setDeletionRecord(record);
+ setDeletionRole(role);
+ setShowDeleteModal(true);
+ }}
+ />
+ )}
+ />
+ {showAddModal && (
+ setShowAddModal(false)}
+ onSave={() => {
+ setShowAddModal(false);
+ fetchAccessRecords();
+ }}
+ roles={resource.summary_fields.object_roles}
/>
- {isAddModalOpen && (
-
- )}
- {isDeleteModalOpen && (
-
- )}
+ )}
+ {showDeleteModal && (
+ {
+ setDeletionRecord(null);
+ setDeletionRole(null);
+ setShowDeleteModal(false);
+ }}
+ onConfirm={async () => {
+ await deleteRole();
+ setShowDeleteModal(false);
+ setDeletionRecord(null);
+ setDeletionRole(null);
+ }}
+ />
+ )}
+ {deletionError && (
{i18n._(t`Failed to delete role`)}
-
- );
- }
+ )}
+ >
+ );
}
-
-export { ResourceAccessList as _ResourceAccessList };
-export default withI18n()(withRouter(ResourceAccessList));
+export default withI18n()(ResourceAccessList);
diff --git a/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.test.jsx b/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.test.jsx
index c4f88e825a..4bdb9c08f0 100644
--- a/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.test.jsx
+++ b/awx/ui_next/src/components/ResourceAccessList/ResourceAccessList.test.jsx
@@ -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('', () => {
+ let wrapper;
const organization = {
id: 1,
name: 'Default',
@@ -74,108 +74,68 @@ describe('', () => {
],
};
- beforeEach(() => {
+ beforeEach(async () => {
OrganizationsAPI.readAccessList.mockResolvedValue({ data });
TeamsAPI.disassociateRole.mockResolvedValue({});
UsersAPI.disassociateRole.mockResolvedValue({});
+ await act(async () => {
+ wrapper = mountWithContexts(
+
+ );
+ });
+ wrapper.update();
});
afterEach(() => {
+ wrapper.unmount();
jest.clearAllMocks();
});
test('initially renders succesfully', () => {
- const wrapper = mountWithContexts(
-
- );
expect(wrapper.find('PaginatedDataList')).toHaveLength(1);
});
test('should fetch and display access records on mount', async done => {
- const wrapper = mountWithContexts(
-
- );
- 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(
-
- );
- 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(
-
- );
- 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(
-
- );
- 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('', () => {
});
it('should delete team role', async done => {
- const wrapper = mountWithContexts(
-
- );
- 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);