mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 18:40:01 -03:30
Merge pull request #5754 from marshmalien/delete-btn-user-team-details
Add delete button to User and Team details Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
commit
473ab7c01c
@ -30,7 +30,7 @@ describe('<HostAdd />', () => {
|
||||
});
|
||||
|
||||
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('<HostAdd />', () => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
@ -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 <ContentLoading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<CardBody>
|
||||
<DetailList>
|
||||
@ -36,12 +59,38 @@ function TeamDetail({ team, i18n }) {
|
||||
/>
|
||||
</DetailList>
|
||||
<CardActionsRow>
|
||||
{summary_fields.user_capabilities.edit && (
|
||||
<Button component={Link} to={`/teams/${id}/edit`}>
|
||||
{i18n._(t`Edit`)}
|
||||
</Button>
|
||||
)}
|
||||
{summary_fields.user_capabilities &&
|
||||
summary_fields.user_capabilities.edit && (
|
||||
<Button
|
||||
aria-label={i18n._(t`Edit`)}
|
||||
component={Link}
|
||||
to={`/teams/${id}/edit`}
|
||||
>
|
||||
{i18n._(t`Edit`)}
|
||||
</Button>
|
||||
)}
|
||||
{summary_fields.user_capabilities &&
|
||||
summary_fields.user_capabilities.delete && (
|
||||
<DeleteButton
|
||||
name={name}
|
||||
modalTitle={i18n._(t`Delete Team`)}
|
||||
onConfirm={handleDelete}
|
||||
>
|
||||
{i18n._(t`Delete`)}
|
||||
</DeleteButton>
|
||||
)}
|
||||
</CardActionsRow>
|
||||
{deletionError && (
|
||||
<AlertModal
|
||||
isOpen={deletionError}
|
||||
variant="danger"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={() => setDeletionError(null)}
|
||||
>
|
||||
{i18n._(t`Failed to delete team.`)}
|
||||
<ErrorDetail error={deletionError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
</CardBody>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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('<TeamDetail />', () => {
|
||||
let wrapper;
|
||||
const mockTeam = {
|
||||
name: 'Foo',
|
||||
description: 'Bar',
|
||||
@ -19,15 +20,25 @@ describe('<TeamDetail />', () => {
|
||||
},
|
||||
user_capabilities: {
|
||||
edit: true,
|
||||
delete: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
test('initially renders succesfully', () => {
|
||||
mountWithContexts(<TeamDetail team={mockTeam} />);
|
||||
|
||||
beforeEach(async () => {
|
||||
wrapper = mountWithContexts(<TeamDetail team={mockTeam} />);
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
});
|
||||
|
||||
test('should render Details', async done => {
|
||||
const wrapper = mountWithContexts(<TeamDetail team={mockTeam} />);
|
||||
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('<TeamDetail />', () => {
|
||||
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(<TeamDetail team={mockTeam} />);
|
||||
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(<TeamDetail team={readOnlyTeam} />);
|
||||
wrapper = mountWithContexts(<TeamDetail team={readOnlyTeam} />);
|
||||
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(<TeamDetail team={mockTeam} />);
|
||||
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
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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 <ContentLoading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<CardBody>
|
||||
<DetailList>
|
||||
@ -52,16 +75,38 @@ function UserDetail({ user, i18n }) {
|
||||
<Detail label={i18n._(t`Created`)} value={formatDateString(created)} />
|
||||
</DetailList>
|
||||
<CardActionsRow>
|
||||
{summary_fields.user_capabilities.edit && (
|
||||
<Button
|
||||
aria-label={i18n._(t`edit`)}
|
||||
component={Link}
|
||||
to={`/users/${id}/edit`}
|
||||
>
|
||||
{i18n._(t`Edit`)}
|
||||
</Button>
|
||||
)}
|
||||
{summary_fields.user_capabilities &&
|
||||
summary_fields.user_capabilities.edit && (
|
||||
<Button
|
||||
aria-label={i18n._(t`edit`)}
|
||||
component={Link}
|
||||
to={`/users/${id}/edit`}
|
||||
>
|
||||
{i18n._(t`Edit`)}
|
||||
</Button>
|
||||
)}
|
||||
{summary_fields.user_capabilities &&
|
||||
summary_fields.user_capabilities.delete && (
|
||||
<DeleteButton
|
||||
name={username}
|
||||
modalTitle={i18n._(t`Delete User`)}
|
||||
onConfirm={handleDelete}
|
||||
>
|
||||
{i18n._(t`Delete`)}
|
||||
</DeleteButton>
|
||||
)}
|
||||
</CardActionsRow>
|
||||
{deletionError && (
|
||||
<AlertModal
|
||||
isOpen={deletionError}
|
||||
variant="danger"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={() => setDeletionError(null)}
|
||||
>
|
||||
{i18n._(t`Failed to delete user.`)}
|
||||
<ErrorDetail error={deletionError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
</CardBody>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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('<UserDetail />', () => {
|
||||
test('initially renders succesfully', () => {
|
||||
mountWithContexts(<UserDetail user={mockDetails} />);
|
||||
@ -39,7 +43,36 @@ describe('<UserDetail />', () => {
|
||||
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(
|
||||
<UserDetail
|
||||
user={{
|
||||
...mockDetails,
|
||||
is_superuser: false,
|
||||
is_system_auditor: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find(`Detail[label="User Type"] dd`).text()).toBe(
|
||||
'System Auditor'
|
||||
);
|
||||
|
||||
wrapper = mountWithContexts(
|
||||
<UserDetail
|
||||
user={{
|
||||
...mockDetails,
|
||||
is_superuser: false,
|
||||
is_system_auditor: false,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
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(<UserDetail user={mockDetails} />);
|
||||
const editButton = await waitForElement(
|
||||
wrapper,
|
||||
@ -47,10 +80,9 @@ describe('<UserDetail />', () => {
|
||||
);
|
||||
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(
|
||||
<UserDetail
|
||||
user={{
|
||||
@ -65,7 +97,6 @@ describe('<UserDetail />', () => {
|
||||
);
|
||||
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('<UserDetail />', () => {
|
||||
.simulate('click', { button: 0 });
|
||||
expect(history.location.pathname).toEqual('/users/1/edit');
|
||||
});
|
||||
|
||||
test('expected api call is made for delete', async () => {
|
||||
const wrapper = mountWithContexts(<UserDetail user={mockDetails} />);
|
||||
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(<UserDetail user={mockDetails} />);
|
||||
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
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
"summary_fields": {
|
||||
"user_capabilities": {
|
||||
"edit": true,
|
||||
"delete": false
|
||||
"delete": true
|
||||
}
|
||||
},
|
||||
"created": "2019-10-28T15:01:07.218634Z",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user