diff --git a/__tests__/pages/Organizations/screens/OrganizationsList.test.jsx b/__tests__/pages/Organizations/screens/OrganizationsList.test.jsx
index 90ed0743b4..cf0cbacfb9 100644
--- a/__tests__/pages/Organizations/screens/OrganizationsList.test.jsx
+++ b/__tests__/pages/Organizations/screens/OrganizationsList.test.jsx
@@ -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('', () => {
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(
- ,
- { context: { router: { history } } }
- );
- await wrapper.setState({
+ test('error is shown when org not successfully deleted from api', async () => {
+ OrganizationsAPI.destroy = () => Promise.reject();
+ wrapper = mountWithContexts();
+ 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!');
});
});
diff --git a/__tests__/pages/Templates/TemplatesList.test.jsx b/__tests__/pages/Templates/TemplatesList.test.jsx
index 38ed7263f8..e70f320190 100644
--- a/__tests__/pages/Templates/TemplatesList.test.jsx
+++ b/__tests__/pages/Templates/TemplatesList.test.jsx
@@ -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('', () => {
});
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();
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('', () => {
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();
+ 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();
+ 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();
+ 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!');
+ });
});
diff --git a/src/api/index.js b/src/api/index.js
index 418b150b04..57429507bb 100644
--- a/src/api/index.js
+++ b/src/api/index.js
@@ -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
};
diff --git a/src/api/models/JobTemplates.js b/src/api/models/JobTemplates.js
new file mode 100644
index 0000000000..3ce27b70ff
--- /dev/null
+++ b/src/api/models/JobTemplates.js
@@ -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;
diff --git a/src/api/models/WorkflowJobTemplates.js b/src/api/models/WorkflowJobTemplates.js
new file mode 100644
index 0000000000..8f553d4eb1
--- /dev/null
+++ b/src/api/models/WorkflowJobTemplates.js
@@ -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;
diff --git a/src/pages/Organizations/screens/OrganizationsList.jsx b/src/pages/Organizations/screens/OrganizationsList.jsx
index 7d72efba76..c9ee097649 100644
--- a/src/pages/Organizations/screens/OrganizationsList.jsx
+++ b/src/pages/Organizations/screens/OrganizationsList.jsx
@@ -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 {
diff --git a/src/pages/Templates/TemplatesList.jsx b/src/pages/Templates/TemplatesList.jsx
index 673c940567..e904fe6846 100644
--- a/src/pages/Templates/TemplatesList.jsx
+++ b/src/pages/Templates/TemplatesList.jsx
@@ -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={[
+
+ ]}
/>
)}
renderItem={(template) => (
@@ -134,6 +174,14 @@ class TemplatesList extends Component {
)}
/>
+
+ {i18n._(t`Failed to delete one or more template.`)}
+
);
}