diff --git a/awx/ui_next/src/screens/Host/HostAdd/HostAdd.test.jsx b/awx/ui_next/src/screens/Host/HostAdd/HostAdd.test.jsx
index 6478856675..2a0e419675 100644
--- a/awx/ui_next/src/screens/Host/HostAdd/HostAdd.test.jsx
+++ b/awx/ui_next/src/screens/Host/HostAdd/HostAdd.test.jsx
@@ -30,7 +30,7 @@ describe('', () => {
});
test('handleSubmit should post to api', async () => {
- act(() => {
+ await act(async () => {
wrapper.find('HostForm').prop('handleSubmit')(hostData);
});
expect(HostsAPI.create).toHaveBeenCalledWith(hostData);
@@ -44,12 +44,14 @@ describe('', () => {
test('successful form submission should trigger redirect', async () => {
HostsAPI.create.mockResolvedValueOnce({
data: {
- id: 5,
...hostData,
+ id: 5,
},
});
await waitForElement(wrapper, 'button[aria-label="Save"]');
- await wrapper.find('HostForm').invoke('handleSubmit')(hostData);
+ await act(async () => {
+ wrapper.find('HostForm').invoke('handleSubmit')(hostData);
+ });
expect(history.location.pathname).toEqual('/hosts/5/details');
});
});
diff --git a/awx/ui_next/src/screens/Team/TeamDetail/TeamDetail.jsx b/awx/ui_next/src/screens/Team/TeamDetail/TeamDetail.jsx
index 7dd3e6b978..90f994aa6b 100644
--- a/awx/ui_next/src/screens/Team/TeamDetail/TeamDetail.jsx
+++ b/awx/ui_next/src/screens/Team/TeamDetail/TeamDetail.jsx
@@ -1,17 +1,40 @@
-import React from 'react';
-import { Link, useParams } from 'react-router-dom';
+import React, { useState } from 'react';
+import { Link, useHistory, useParams } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { Button } from '@patternfly/react-core';
+import AlertModal from '@components/AlertModal';
import { CardBody, CardActionsRow } from '@components/Card';
+import ContentLoading from '@components/ContentLoading';
+import DeleteButton from '@components/DeleteButton';
import { DetailList, Detail } from '@components/DetailList';
+import ErrorDetail from '@components/ErrorDetail';
import { formatDateString } from '@util/dates';
+import { TeamsAPI } from '@api';
function TeamDetail({ team, i18n }) {
const { name, description, created, modified, summary_fields } = team;
+ const [deletionError, setDeletionError] = useState(null);
+ const [hasContentLoading, setHasContentLoading] = useState(false);
+ const history = useHistory();
const { id } = useParams();
+ const handleDelete = async () => {
+ setHasContentLoading(true);
+ try {
+ await TeamsAPI.destroy(id);
+ history.push(`/teams`);
+ } catch (error) {
+ setDeletionError(error);
+ }
+ setHasContentLoading(false);
+ };
+
+ if (hasContentLoading) {
+ return ;
+ }
+
return (
@@ -36,12 +59,38 @@ function TeamDetail({ team, i18n }) {
/>
- {summary_fields.user_capabilities.edit && (
-
- )}
+ {summary_fields.user_capabilities &&
+ summary_fields.user_capabilities.edit && (
+
+ )}
+ {summary_fields.user_capabilities &&
+ summary_fields.user_capabilities.delete && (
+
+ {i18n._(t`Delete`)}
+
+ )}
+ {deletionError && (
+ setDeletionError(null)}
+ >
+ {i18n._(t`Failed to delete team.`)}
+
+
+ )}
);
}
diff --git a/awx/ui_next/src/screens/Team/TeamDetail/TeamDetail.test.jsx b/awx/ui_next/src/screens/Team/TeamDetail/TeamDetail.test.jsx
index 5b971c454c..420985a307 100644
--- a/awx/ui_next/src/screens/Team/TeamDetail/TeamDetail.test.jsx
+++ b/awx/ui_next/src/screens/Team/TeamDetail/TeamDetail.test.jsx
@@ -1,12 +1,13 @@
import React from 'react';
-
+import { act } from 'react-dom/test-utils';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
-
import TeamDetail from './TeamDetail';
+import { TeamsAPI } from '@api';
jest.mock('@api');
describe('', () => {
+ let wrapper;
const mockTeam = {
name: 'Foo',
description: 'Bar',
@@ -19,15 +20,25 @@ describe('', () => {
},
user_capabilities: {
edit: true,
+ delete: true,
},
},
};
- test('initially renders succesfully', () => {
- mountWithContexts();
+
+ beforeEach(async () => {
+ wrapper = mountWithContexts();
+ await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
});
- test('should render Details', async done => {
- const wrapper = mountWithContexts();
+ afterEach(() => {
+ wrapper.unmount();
+ });
+
+ test('initially renders succesfully', async () => {
+ await waitForElement(wrapper, 'TeamDetail');
+ });
+
+ test('should render Details', async () => {
const testParams = [
{ label: 'Name', value: 'Foo' },
{ label: 'Description', value: 'Bar' },
@@ -42,23 +53,52 @@ describe('', () => {
expect(detail.find('dt').text()).toBe(label);
expect(detail.find('dd').text()).toBe(value);
}
- done();
});
- test('should show edit button for users with edit permission', async done => {
- const wrapper = mountWithContexts();
- const editButton = await waitForElement(wrapper, 'TeamDetail Button');
+ test('should show edit button for users with edit permission', async () => {
+ const editButton = await waitForElement(
+ wrapper,
+ 'TeamDetail Button[aria-label="Edit"]'
+ );
expect(editButton.text()).toEqual('Edit');
expect(editButton.prop('to')).toBe('/teams/undefined/edit');
- done();
});
- test('should hide edit button for users without edit permission', async done => {
+ test('should hide edit button for users without edit permission', async () => {
const readOnlyTeam = { ...mockTeam };
readOnlyTeam.summary_fields.user_capabilities.edit = false;
- const wrapper = mountWithContexts();
+ wrapper = mountWithContexts();
await waitForElement(wrapper, 'TeamDetail');
- expect(wrapper.find('TeamDetail Button').length).toBe(0);
- done();
+ expect(wrapper.find('TeamDetail Button[aria-label="Edit"]').length).toBe(0);
+ });
+
+ test('expected api call is made for delete', async () => {
+ await waitForElement(wrapper, 'TeamDetail Button[aria-label="Delete"]');
+ await act(async () => {
+ wrapper.find('DeleteButton').invoke('onConfirm')();
+ });
+ expect(TeamsAPI.destroy).toHaveBeenCalledTimes(1);
+ });
+
+ test('Error dialog shown for failed deletion', async () => {
+ TeamsAPI.destroy.mockImplementationOnce(() => Promise.reject(new Error()));
+ wrapper = mountWithContexts();
+ await waitForElement(wrapper, 'TeamDetail Button[aria-label="Delete"]');
+ await act(async () => {
+ wrapper.find('DeleteButton').invoke('onConfirm')();
+ });
+ await waitForElement(
+ wrapper,
+ 'Modal[title="Error!"]',
+ el => el.length === 1
+ );
+ await act(async () => {
+ wrapper.find('Modal[title="Error!"]').invoke('onClose')();
+ });
+ await waitForElement(
+ wrapper,
+ 'Modal[title="Error!"]',
+ el => el.length === 0
+ );
});
});
diff --git a/awx/ui_next/src/screens/User/UserDetail/UserDetail.jsx b/awx/ui_next/src/screens/User/UserDetail/UserDetail.jsx
index 840a39ed1d..72c4bdab6a 100644
--- a/awx/ui_next/src/screens/User/UserDetail/UserDetail.jsx
+++ b/awx/ui_next/src/screens/User/UserDetail/UserDetail.jsx
@@ -1,12 +1,17 @@
-import React from 'react';
-import { Link } from 'react-router-dom';
+import React, { useState } from 'react';
+import { Link, useHistory } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
-import { Button } from '@patternfly/react-core';
+import AlertModal from '@components/AlertModal';
+import { Button } from '@patternfly/react-core';
import { CardBody, CardActionsRow } from '@components/Card';
+import ContentLoading from '@components/ContentLoading';
+import DeleteButton from '@components/DeleteButton';
import { DetailList, Detail } from '@components/DetailList';
+import ErrorDetail from '@components/ErrorDetail';
import { formatDateString } from '@util/dates';
+import { UsersAPI } from '@api';
function UserDetail({ user, i18n }) {
const {
@@ -21,6 +26,20 @@ function UserDetail({ user, i18n }) {
is_system_auditor,
summary_fields,
} = user;
+ const [deletionError, setDeletionError] = useState(null);
+ const [hasContentLoading, setHasContentLoading] = useState(false);
+ const history = useHistory();
+
+ const handleDelete = async () => {
+ setHasContentLoading(true);
+ try {
+ await UsersAPI.destroy(id);
+ history.push(`/users`);
+ } catch (error) {
+ setDeletionError(error);
+ }
+ setHasContentLoading(false);
+ };
let user_type;
if (is_superuser) {
@@ -31,6 +50,10 @@ function UserDetail({ user, i18n }) {
user_type = i18n._(t`Normal User`);
}
+ if (hasContentLoading) {
+ return ;
+ }
+
return (
@@ -52,16 +75,38 @@ function UserDetail({ user, i18n }) {
- {summary_fields.user_capabilities.edit && (
-
- )}
+ {summary_fields.user_capabilities &&
+ summary_fields.user_capabilities.edit && (
+
+ )}
+ {summary_fields.user_capabilities &&
+ summary_fields.user_capabilities.delete && (
+
+ {i18n._(t`Delete`)}
+
+ )}
+ {deletionError && (
+ setDeletionError(null)}
+ >
+ {i18n._(t`Failed to delete user.`)}
+
+
+ )}
);
}
diff --git a/awx/ui_next/src/screens/User/UserDetail/UserDetail.test.jsx b/awx/ui_next/src/screens/User/UserDetail/UserDetail.test.jsx
index 7e3b379e48..69ec4f36e7 100644
--- a/awx/ui_next/src/screens/User/UserDetail/UserDetail.test.jsx
+++ b/awx/ui_next/src/screens/User/UserDetail/UserDetail.test.jsx
@@ -1,9 +1,13 @@
import React from 'react';
+import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
+import { UsersAPI } from '@api';
import UserDetail from './UserDetail';
import mockDetails from '../data.user.json';
+jest.mock('@api');
+
describe('', () => {
test('initially renders succesfully', () => {
mountWithContexts();
@@ -39,7 +43,36 @@ describe('', () => {
assertDetail('Created', `10/28/2019, 3:01:07 PM`);
});
- test('should show edit button for users with edit permission', async done => {
+ test('User Type Detail should render expected strings', async () => {
+ let wrapper;
+ wrapper = mountWithContexts(
+
+ );
+ expect(wrapper.find(`Detail[label="User Type"] dd`).text()).toBe(
+ 'System Auditor'
+ );
+
+ wrapper = mountWithContexts(
+
+ );
+ expect(wrapper.find(`Detail[label="User Type"] dd`).text()).toBe(
+ 'Normal User'
+ );
+ });
+
+ test('should show edit button for users with edit permission', async () => {
const wrapper = mountWithContexts();
const editButton = await waitForElement(
wrapper,
@@ -47,10 +80,9 @@ describe('', () => {
);
expect(editButton.text()).toEqual('Edit');
expect(editButton.prop('to')).toBe(`/users/${mockDetails.id}/edit`);
- done();
});
- test('should hide edit button for users without edit permission', async done => {
+ test('should hide edit button for users without edit permission', async () => {
const wrapper = mountWithContexts(
', () => {
);
await waitForElement(wrapper, 'UserDetail');
expect(wrapper.find('UserDetail Button[aria-label="edit"]').length).toBe(0);
- done();
});
test('edit button should navigate to user edit', () => {
@@ -79,4 +110,35 @@ describe('', () => {
.simulate('click', { button: 0 });
expect(history.location.pathname).toEqual('/users/1/edit');
});
+
+ test('expected api call is made for delete', async () => {
+ const wrapper = mountWithContexts();
+ await waitForElement(wrapper, 'UserDetail Button[aria-label="Delete"]');
+ await act(async () => {
+ wrapper.find('DeleteButton').invoke('onConfirm')();
+ });
+ expect(UsersAPI.destroy).toHaveBeenCalledTimes(1);
+ });
+
+ test('Error dialog shown for failed deletion', async () => {
+ UsersAPI.destroy.mockImplementationOnce(() => Promise.reject(new Error()));
+ const wrapper = mountWithContexts();
+ await waitForElement(wrapper, 'UserDetail Button[aria-label="Delete"]');
+ await act(async () => {
+ wrapper.find('DeleteButton').invoke('onConfirm')();
+ });
+ await waitForElement(
+ wrapper,
+ 'Modal[title="Error!"]',
+ el => el.length === 1
+ );
+ await act(async () => {
+ wrapper.find('Modal[title="Error!"]').invoke('onClose')();
+ });
+ await waitForElement(
+ wrapper,
+ 'Modal[title="Error!"]',
+ el => el.length === 0
+ );
+ });
});
diff --git a/awx/ui_next/src/screens/User/data.user.json b/awx/ui_next/src/screens/User/data.user.json
index 3ab136a9b6..fc71d1f128 100644
--- a/awx/ui_next/src/screens/User/data.user.json
+++ b/awx/ui_next/src/screens/User/data.user.json
@@ -18,7 +18,7 @@
"summary_fields": {
"user_capabilities": {
"edit": true,
- "delete": false
+ "delete": true
}
},
"created": "2019-10-28T15:01:07.218634Z",