1
0
mirror of https://github.com/ansible/awx.git synced 2024-10-31 23:51:09 +03:00

Add support for deleting templates on templates list (#266)

Adds support for deleting templates from the templates list
This commit is contained in:
Michael Abashian 2019-06-17 13:52:05 -04:00 committed by GitHub
parent 4e45a3b365
commit 7a5cf4b81c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 174 additions and 40 deletions

View File

@ -1,6 +1,5 @@
import React from 'react';
import { createMemoryHistory } from 'history';
import { mountWithContexts } from '../../../enzymeHelpers';
import { mountWithContexts, waitForElement } from '../../../enzymeHelpers';
import OrganizationsList, { _OrganizationsList } from '../../../../src/pages/Organizations/screens/OrganizationsList';
import { OrganizationsAPI } from '../../../../src/api';
@ -122,25 +121,16 @@ describe('<OrganizationsList />', () => {
expect(fetchOrgs).toBeCalled();
});
test('error is thrown when org not successfully deleted from api', async () => {
const history = createMemoryHistory({
initialEntries: ['organizations?order_by=name&page=1&page_size=5'],
});
wrapper = mountWithContexts(
<OrganizationsList />,
{ context: { router: { history } } }
);
await wrapper.setState({
test('error is shown when org not successfully deleted from api', async () => {
OrganizationsAPI.destroy = () => Promise.reject();
wrapper = mountWithContexts(<OrganizationsList />);
wrapper.find('OrganizationsList').setState({
organizations: mockAPIOrgsList.data.results,
itemCount: 3,
isInitialized: true,
selected: [...mockAPIOrgsList.data.results].push({
name: 'Organization 6',
id: 'a',
})
selected: mockAPIOrgsList.data.results.slice(0, 1)
});
wrapper.update();
const component = wrapper.find('OrganizationsList');
component.instance().handleOrgDelete();
wrapper.find('ToolbarDeleteButton').prop('onDelete')();
await waitForElement(wrapper, 'Modal', (el) => el.props().isOpen === true && el.props().title === 'Error!');
});
});

View File

@ -1,38 +1,63 @@
import React from 'react';
import { mountWithContexts, waitForElement } from '../../enzymeHelpers';
import TemplatesList, { _TemplatesList } from '../../../src/pages/Templates/TemplatesList';
import { UnifiedJobTemplatesAPI } from '../../../src/api';
import { JobTemplatesAPI, UnifiedJobTemplatesAPI, WorkflowJobTemplatesAPI } from '../../../src/api';
jest.mock('../../../src/api');
const mockTemplates = [{
id: 1,
name: 'Template 1',
name: 'Job Template 1',
url: '/templates/job_template/1',
type: 'job_template',
summary_fields: {
inventory: {},
project: {},
user_capabilities: {
delete: true
}
}
},
{
id: 2,
name: 'Template 2',
name: 'Job Template 2',
url: '/templates/job_template/2',
type: 'job_template',
summary_fields: {
inventory: {},
project: {},
user_capabilities: {
delete: true
}
}
},
{
id: 3,
name: 'Template 3',
name: 'Job Template 3',
url: '/templates/job_template/3',
type: 'job_template',
summary_fields: {
inventory: {},
project: {},
user_capabilities: {
delete: true
}
}
},
{
id: 4,
name: 'Workflow Job Template 1',
url: '/templates/workflow_job_template/4',
type: 'workflow_job_template',
summary_fields: {
user_capabilities: {
delete: true
}
}
},
{
id: 5,
name: 'Workflow Job Template 2',
url: '/templates/workflow_job_template/5',
type: 'workflow_job_template',
summary_fields: {
user_capabilities: {
delete: false
}
}
}];
@ -60,10 +85,10 @@ describe('<TemplatesList />', () => {
});
test('Templates are retrieved from the api and the components finishes loading', async (done) => {
const loadUnifiedJobTemplates = jest.spyOn(_TemplatesList.prototype, 'loadUnifiedJobTemplates');
const loadTemplates = jest.spyOn(_TemplatesList.prototype, 'loadTemplates');
const wrapper = mountWithContexts(<TemplatesList />);
await waitForElement(wrapper, 'TemplatesList', (el) => el.state('contentLoading') === true);
expect(loadUnifiedJobTemplates).toHaveBeenCalled();
expect(loadTemplates).toHaveBeenCalled();
await waitForElement(wrapper, 'TemplatesList', (el) => el.state('contentLoading') === false);
done();
});
@ -84,7 +109,53 @@ describe('<TemplatesList />', () => {
await waitForElement(wrapper, 'TemplatesList', (el) => el.state('contentLoading') === false);
wrapper.find('Checkbox#select-all').props().onChange(true);
expect(handleSelectAll).toBeCalled();
await waitForElement(wrapper, 'TemplatesList', (el) => el.state('selected').length === 3);
await waitForElement(wrapper, 'TemplatesList', (el) => el.state('selected').length === 5);
done();
});
test('delete button is disabled if user does not have delete capabilities on a selected template', async (done) => {
const wrapper = mountWithContexts(<TemplatesList />);
wrapper.find('TemplatesList').setState({
templates: mockTemplates,
itemCount: 5,
isInitialized: true,
selected: mockTemplates.slice(0, 4)
});
await waitForElement(wrapper, 'ToolbarDeleteButton * button', (el) => el.getDOMNode().disabled === false);
wrapper.find('TemplatesList').setState({
selected: mockTemplates
});
await waitForElement(wrapper, 'ToolbarDeleteButton * button', (el) => el.getDOMNode().disabled === true);
done();
});
test('api is called to delete templates for each selected template.', () => {
JobTemplatesAPI.destroy = jest.fn();
WorkflowJobTemplatesAPI.destroy = jest.fn();
const wrapper = mountWithContexts(<TemplatesList />);
wrapper.find('TemplatesList').setState({
templates: mockTemplates,
itemCount: 5,
isInitialized: true,
isModalOpen: true,
selected: mockTemplates.slice(0, 4)
});
wrapper.find('ToolbarDeleteButton').prop('onDelete')();
expect(JobTemplatesAPI.destroy).toHaveBeenCalledTimes(3);
expect(WorkflowJobTemplatesAPI.destroy).toHaveBeenCalledTimes(1);
});
test('error is shown when template not successfully deleted from api', async () => {
JobTemplatesAPI.destroy = () => Promise.reject();
const wrapper = mountWithContexts(<TemplatesList />);
wrapper.find('TemplatesList').setState({
templates: mockTemplates,
itemCount: 1,
isInitialized: true,
isModalOpen: true,
selected: mockTemplates.slice(0, 1)
});
wrapper.find('ToolbarDeleteButton').prop('onDelete')();
await waitForElement(wrapper, 'Modal', (el) => el.props().isOpen === true && el.props().title === 'Error!');
});
});

View File

@ -1,5 +1,6 @@
import Config from './models/Config';
import InstanceGroups from './models/InstanceGroups';
import JobTemplates from './models/JobTemplates';
import Jobs from './models/Jobs';
import Me from './models/Me';
import Organizations from './models/Organizations';
@ -7,9 +8,11 @@ import Root from './models/Root';
import Teams from './models/Teams';
import UnifiedJobTemplates from './models/UnifiedJobTemplates';
import Users from './models/Users';
import WorkflowJobTemplates from './models/WorkflowJobTemplates';
const ConfigAPI = new Config();
const InstanceGroupsAPI = new InstanceGroups();
const JobTemplatesAPI = new JobTemplates();
const JobsAPI = new Jobs();
const MeAPI = new Me();
const OrganizationsAPI = new Organizations();
@ -17,15 +20,18 @@ const RootAPI = new Root();
const TeamsAPI = new Teams();
const UnifiedJobTemplatesAPI = new UnifiedJobTemplates();
const UsersAPI = new Users();
const WorkflowJobTemplatesAPI = new WorkflowJobTemplates();
export {
ConfigAPI,
InstanceGroupsAPI,
JobTemplatesAPI,
JobsAPI,
MeAPI,
OrganizationsAPI,
RootAPI,
TeamsAPI,
UnifiedJobTemplatesAPI,
UsersAPI
UsersAPI,
WorkflowJobTemplatesAPI
};

View File

@ -0,0 +1,10 @@
import Base from '../Base';
class JobTemplates extends Base {
constructor (http) {
super(http);
this.baseUrl = '/api/v2/job_templates/';
}
}
export default JobTemplates;

View File

@ -0,0 +1,10 @@
import Base from '../Base';
class WorkflowJobTemplates extends Base {
constructor (http) {
super(http);
this.baseUrl = '/api/v2/workflow_job_templates/';
}
}
export default WorkflowJobTemplates;

View File

@ -83,7 +83,6 @@ class OrganizationsList extends Component {
this.setState({ contentLoading: true, deletionError: false });
try {
await Promise.all(selected.map((org) => OrganizationsAPI.destroy(org.id)));
this.setState({ selected: [] });
} catch (err) {
this.setState({ deletionError: true });
} finally {

View File

@ -7,11 +7,14 @@ import {
PageSection,
PageSectionVariants,
} from '@patternfly/react-core';
import { UnifiedJobTemplatesAPI } from '../../api';
import { JobTemplatesAPI, UnifiedJobTemplatesAPI, WorkflowJobTemplatesAPI } from '../../api';
import { getQSConfig, parseNamespacedQueryString } from '../../util/qs';
import AlertModal from '../../components/AlertModal';
import DatalistToolbar from '../../components/DataListToolbar';
import PaginatedDataList from '../../components/PaginatedDataList';
import PaginatedDataList, {
ToolbarDeleteButton
} from '../../components/PaginatedDataList';
import TemplateListItem from './components/TemplateListItem';
// The type value in const QS_CONFIG below does not have a space between job_template and
@ -28,28 +31,35 @@ class TemplatesList extends Component {
super(props);
this.state = {
contentError: false,
contentLoading: true,
contentError: false,
deletionError: false,
selected: [],
templates: [],
itemCount: 0,
};
this.loadUnifiedJobTemplates = this.loadUnifiedJobTemplates.bind(this);
this.loadTemplates = this.loadTemplates.bind(this);
this.handleSelectAll = this.handleSelectAll.bind(this);
this.handleSelect = this.handleSelect.bind(this);
this.handleTemplateDelete = this.handleTemplateDelete.bind(this);
this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this);
}
componentDidMount () {
this.loadUnifiedJobTemplates();
this.loadTemplates();
}
componentDidUpdate (prevProps) {
const { location } = this.props;
if (location !== prevProps.location) {
this.loadUnifiedJobTemplates();
this.loadTemplates();
}
}
handleDeleteErrorClose () {
this.setState({ deletionError: false });
}
handleSelectAll (isSelected) {
const { templates } = this.state;
const selected = isSelected ? [...templates] : [];
@ -65,7 +75,28 @@ class TemplatesList extends Component {
}
}
async loadUnifiedJobTemplates () {
async handleTemplateDelete () {
const { selected } = this.state;
this.setState({ contentLoading: true, deletionError: false });
try {
await Promise.all(selected.map(({ type, id }) => {
let deletePromise;
if (type === 'job_template') {
deletePromise = JobTemplatesAPI.destroy(id);
} else if (type === 'workflow_job_template') {
deletePromise = WorkflowJobTemplatesAPI.destroy(id);
}
return deletePromise;
}));
} catch (err) {
this.setState({ deletionError: true });
} finally {
await this.loadTemplates();
}
}
async loadTemplates () {
const { location } = this.props;
const params = parseNamespacedQueryString(QS_CONFIG, location.search);
@ -88,6 +119,7 @@ class TemplatesList extends Component {
const {
contentError,
contentLoading,
deletionError,
templates,
itemCount,
selected,
@ -120,6 +152,14 @@ class TemplatesList extends Component {
showExpandCollapse
isAllSelected={isAllSelected}
onSelectAll={this.handleSelectAll}
additionalControls={[
<ToolbarDeleteButton
key="delete"
onDelete={this.handleTemplateDelete}
itemsToDelete={selected}
itemName={i18n._(t`Template`)}
/>
]}
/>
)}
renderItem={(template) => (
@ -134,6 +174,14 @@ class TemplatesList extends Component {
)}
/>
</Card>
<AlertModal
isOpen={deletionError}
variant="danger"
title={i18n._(t`Error!`)}
onClose={this.handleDeleteErrorClose}
>
{i18n._(t`Failed to delete one or more template.`)}
</AlertModal>
</PageSection>
);
}