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",