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

Hook up delete on jobs list. Add more comprehensive error handling on delete in organization and template lists.

This commit is contained in:
mabashian 2019-08-22 11:22:37 -04:00
parent 1d05c21af4
commit 5549dac17d
11 changed files with 222 additions and 33 deletions

View File

@ -1,49 +1,64 @@
import AdHocCommands from './models/AdHocCommands';
import Config from './models/Config'; import Config from './models/Config';
import InstanceGroups from './models/InstanceGroups'; import InstanceGroups from './models/InstanceGroups';
import Inventories from './models/Inventories'; import Inventories from './models/Inventories';
import InventoryUpdates from './models/InventoryUpdates';
import JobTemplates from './models/JobTemplates'; import JobTemplates from './models/JobTemplates';
import Jobs from './models/Jobs'; import Jobs from './models/Jobs';
import Labels from './models/Labels'; import Labels from './models/Labels';
import Me from './models/Me'; import Me from './models/Me';
import Organizations from './models/Organizations'; import Organizations from './models/Organizations';
import Projects from './models/Projects'; import Projects from './models/Projects';
import ProjectUpdates from './models/ProjectUpdates';
import Root from './models/Root'; import Root from './models/Root';
import SystemJobs from './models/SystemJobs';
import Teams from './models/Teams'; import Teams from './models/Teams';
import UnifiedJobTemplates from './models/UnifiedJobTemplates'; import UnifiedJobTemplates from './models/UnifiedJobTemplates';
import UnifiedJobs from './models/UnifiedJobs'; import UnifiedJobs from './models/UnifiedJobs';
import Users from './models/Users'; import Users from './models/Users';
import WorkflowJobs from './models/WorkflowJobs';
import WorkflowJobTemplates from './models/WorkflowJobTemplates'; import WorkflowJobTemplates from './models/WorkflowJobTemplates';
const AdHocCommandsAPI = new AdHocCommands();
const ConfigAPI = new Config(); const ConfigAPI = new Config();
const InstanceGroupsAPI = new InstanceGroups(); const InstanceGroupsAPI = new InstanceGroups();
const InventoriesAPI = new Inventories(); const InventoriesAPI = new Inventories();
const InventoryUpdatesAPI = new InventoryUpdates();
const JobTemplatesAPI = new JobTemplates(); const JobTemplatesAPI = new JobTemplates();
const JobsAPI = new Jobs(); const JobsAPI = new Jobs();
const LabelsAPI = new Labels(); const LabelsAPI = new Labels();
const MeAPI = new Me(); const MeAPI = new Me();
const OrganizationsAPI = new Organizations(); const OrganizationsAPI = new Organizations();
const ProjectsAPI = new Projects(); const ProjectsAPI = new Projects();
const ProjectUpdatesAPI = new ProjectUpdates();
const RootAPI = new Root(); const RootAPI = new Root();
const SystemJobsAPI = new SystemJobs();
const TeamsAPI = new Teams(); const TeamsAPI = new Teams();
const UnifiedJobTemplatesAPI = new UnifiedJobTemplates(); const UnifiedJobTemplatesAPI = new UnifiedJobTemplates();
const UnifiedJobsAPI = new UnifiedJobs(); const UnifiedJobsAPI = new UnifiedJobs();
const UsersAPI = new Users(); const UsersAPI = new Users();
const WorkflowJobsAPI = new WorkflowJobs();
const WorkflowJobTemplatesAPI = new WorkflowJobTemplates(); const WorkflowJobTemplatesAPI = new WorkflowJobTemplates();
export { export {
AdHocCommandsAPI,
ConfigAPI, ConfigAPI,
InstanceGroupsAPI, InstanceGroupsAPI,
InventoriesAPI, InventoriesAPI,
InventoryUpdatesAPI,
JobTemplatesAPI, JobTemplatesAPI,
JobsAPI, JobsAPI,
LabelsAPI, LabelsAPI,
MeAPI, MeAPI,
OrganizationsAPI, OrganizationsAPI,
ProjectsAPI, ProjectsAPI,
ProjectUpdatesAPI,
RootAPI, RootAPI,
SystemJobsAPI,
TeamsAPI, TeamsAPI,
UnifiedJobTemplatesAPI, UnifiedJobTemplatesAPI,
UnifiedJobsAPI, UnifiedJobsAPI,
UsersAPI, UsersAPI,
WorkflowJobsAPI,
WorkflowJobTemplatesAPI, WorkflowJobTemplatesAPI,
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,9 +4,18 @@ import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Card, PageSection, PageSectionVariants } from '@patternfly/react-core'; import { Card, PageSection, PageSectionVariants } from '@patternfly/react-core';
import { UnifiedJobsAPI } from '@api'; import {
AdHocCommandsAPI,
InventoryUpdatesAPI,
JobsAPI,
ProjectUpdatesAPI,
SystemJobsAPI,
UnifiedJobsAPI,
WorkflowJobsAPI,
} from '@api';
import AlertModal from '@components/AlertModal'; import AlertModal from '@components/AlertModal';
import DatalistToolbar from '@components/DataListToolbar'; import DatalistToolbar from '@components/DataListToolbar';
import ErrorDetail from '@components/ErrorDetail';
import PaginatedDataList, { import PaginatedDataList, {
ToolbarDeleteButton, ToolbarDeleteButton,
} from '@components/PaginatedDataList'; } from '@components/PaginatedDataList';
@ -27,8 +36,8 @@ class JobList extends Component {
this.state = { this.state = {
hasContentLoading: true, hasContentLoading: true,
deletionError: null,
contentError: null, contentError: null,
deletionError: false,
selected: [], selected: [],
jobs: [], jobs: [],
itemCount: 0, itemCount: 0,
@ -36,7 +45,7 @@ class JobList extends Component {
this.loadJobs = this.loadJobs.bind(this); this.loadJobs = this.loadJobs.bind(this);
this.handleSelectAll = this.handleSelectAll.bind(this); this.handleSelectAll = this.handleSelectAll.bind(this);
this.handleSelect = this.handleSelect.bind(this); this.handleSelect = this.handleSelect.bind(this);
this.handleDelete = this.handleDelete.bind(this); this.handleJobDelete = this.handleJobDelete.bind(this);
this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this); this.handleDeleteErrorClose = this.handleDeleteErrorClose.bind(this);
} }
@ -52,7 +61,7 @@ class JobList extends Component {
} }
handleDeleteErrorClose() { handleDeleteErrorClose() {
this.setState({ deletionError: false }); this.setState({ deletionError: null });
} }
handleSelectAll(isSelected) { handleSelectAll(isSelected) {
@ -70,13 +79,41 @@ class JobList extends Component {
} }
} }
async handleDelete() { async handleJobDelete() {
const { selected } = this.state; const { selected, itemCount } = this.state;
this.setState({ hasContentLoading: true, deletionError: false }); this.setState({ hasContentLoading: true });
try { try {
await Promise.all(selected.map(({ id }) => UnifiedJobsAPI.destroy(id))); await Promise.all(
selected.map(({ type, id }) => {
let deletePromise;
switch (type) {
case 'job':
deletePromise = JobsAPI.destroy(id);
break;
case 'ad_hoc_command':
deletePromise = AdHocCommandsAPI.destroy(id);
break;
case 'system_job':
deletePromise = SystemJobsAPI.destroy(id);
break;
case 'project_update':
deletePromise = ProjectUpdatesAPI.destroy(id);
break;
case 'inventory_update':
deletePromise = InventoryUpdatesAPI.destroy(id);
break;
case 'workflow_job':
deletePromise = WorkflowJobsAPI.destroy(id);
break;
default:
break;
}
return deletePromise;
})
);
this.setState({ itemCount: itemCount - selected.length });
} catch (err) { } catch (err) {
this.setState({ deletionError: true }); this.setState({ deletionError: err });
} finally { } finally {
await this.loadJobs(); await this.loadJobs();
} }
@ -150,7 +187,7 @@ class JobList extends Component {
additionalControls={[ additionalControls={[
<ToolbarDeleteButton <ToolbarDeleteButton
key="delete" key="delete"
onDelete={this.handleDelete} onDelete={this.handleJobDelete}
itemsToDelete={selected} itemsToDelete={selected}
itemName={itemName} itemName={itemName}
/>, />,
@ -176,6 +213,7 @@ class JobList extends Component {
onClose={this.handleDeleteErrorClose} onClose={this.handleDeleteErrorClose}
> >
{i18n._(t`Failed to delete one or more jobs.`)} {i18n._(t`Failed to delete one or more jobs.`)}
<ErrorDetail error={deletionError} />
</AlertModal> </AlertModal>
</PageSection> </PageSection>
); );

View File

@ -1,7 +1,15 @@
import React from 'react'; import React from 'react';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import { UnifiedJobsAPI } from '@api'; import {
AdHocCommandsAPI,
InventoryUpdatesAPI,
JobsAPI,
ProjectUpdatesAPI,
SystemJobsAPI,
UnifiedJobsAPI,
WorkflowJobsAPI,
} from '@api';
import JobList from './JobList'; import JobList from './JobList';
jest.mock('@api'); jest.mock('@api');
@ -11,7 +19,7 @@ const mockResults = [
id: 1, id: 1,
url: '/api/v2/project_updates/1', url: '/api/v2/project_updates/1',
name: 'job 1', name: 'job 1',
type: 'project update', type: 'project_update',
summary_fields: { summary_fields: {
user_capabilities: { user_capabilities: {
delete: true, delete: true,
@ -31,9 +39,42 @@ const mockResults = [
}, },
{ {
id: 3, id: 3,
url: '/api/v2/jobs/3', url: '/api/v2/inventory_updates/3',
name: 'job 3', name: 'job 3',
type: 'job', type: 'inventory_update',
summary_fields: {
user_capabilities: {
delete: true,
},
},
},
{
id: 4,
url: '/api/v2/workflow_jobs/4',
name: 'job 4',
type: 'workflow_job',
summary_fields: {
user_capabilities: {
delete: true,
},
},
},
{
id: 5,
url: '/api/v2/system_jobs/5',
name: 'job 5',
type: 'system_job',
summary_fields: {
user_capabilities: {
delete: true,
},
},
},
{
id: 6,
url: '/api/v2/ad_hoc_commands/6',
name: 'job 6',
type: 'ad_hoc_command',
summary_fields: { summary_fields: {
user_capabilities: { user_capabilities: {
delete: true, delete: true,
@ -52,7 +93,7 @@ describe('<JobList />', () => {
await waitForElement( await waitForElement(
wrapper, wrapper,
'JobList', 'JobList',
el => el.state('jobs').length === 3 el => el.state('jobs').length === 6
); );
done(); done();
@ -61,7 +102,7 @@ describe('<JobList />', () => {
test('select makes expected state updates', async done => { test('select makes expected state updates', async done => {
const [mockItem] = mockResults; const [mockItem] = mockResults;
const wrapper = mountWithContexts(<JobList />); const wrapper = mountWithContexts(<JobList />);
await waitForElement(wrapper, 'JobListItem', el => el.length === 3); await waitForElement(wrapper, 'JobListItem', el => el.length === 6);
wrapper wrapper
.find('JobListItem') .find('JobListItem')
@ -79,20 +120,59 @@ describe('<JobList />', () => {
}); });
test('select-all-delete makes expected state updates and api calls', async done => { test('select-all-delete makes expected state updates and api calls', async done => {
AdHocCommandsAPI.destroy = jest.fn();
InventoryUpdatesAPI.destroy = jest.fn();
JobsAPI.destroy = jest.fn();
ProjectUpdatesAPI.destroy = jest.fn();
SystemJobsAPI.destroy = jest.fn();
WorkflowJobsAPI.destroy = jest.fn();
const wrapper = mountWithContexts(<JobList />); const wrapper = mountWithContexts(<JobList />);
await waitForElement(wrapper, 'JobListItem', el => el.length === 3); await waitForElement(wrapper, 'JobListItem', el => el.length === 6);
wrapper.find('DataListToolbar').prop('onSelectAll')(true); wrapper.find('DataListToolbar').prop('onSelectAll')(true);
expect(wrapper.find('JobList').state('selected').length).toEqual(3); expect(wrapper.find('JobList').state('selected').length).toEqual(6);
wrapper.find('DataListToolbar').prop('onSelectAll')(false); wrapper.find('DataListToolbar').prop('onSelectAll')(false);
expect(wrapper.find('JobList').state('selected').length).toEqual(0); expect(wrapper.find('JobList').state('selected').length).toEqual(0);
wrapper.find('DataListToolbar').prop('onSelectAll')(true); wrapper.find('DataListToolbar').prop('onSelectAll')(true);
expect(wrapper.find('JobList').state('selected').length).toEqual(3); expect(wrapper.find('JobList').state('selected').length).toEqual(6);
wrapper.find('ToolbarDeleteButton').prop('onDelete')(); wrapper.find('ToolbarDeleteButton').prop('onDelete')();
expect(UnifiedJobsAPI.destroy).toHaveBeenCalledTimes(3); expect(AdHocCommandsAPI.destroy).toHaveBeenCalledTimes(1);
expect(InventoryUpdatesAPI.destroy).toHaveBeenCalledTimes(1);
expect(JobsAPI.destroy).toHaveBeenCalledTimes(1);
expect(ProjectUpdatesAPI.destroy).toHaveBeenCalledTimes(1);
expect(SystemJobsAPI.destroy).toHaveBeenCalledTimes(1);
expect(WorkflowJobsAPI.destroy).toHaveBeenCalledTimes(1);
done();
});
test('error is shown when job not successfully deleted from api', async done => {
JobsAPI.destroy.mockRejectedValue(
new Error({
response: {
config: {
method: 'delete',
url: '/api/v2/jobs/2',
},
data: 'An error occurred',
},
})
);
const wrapper = mountWithContexts(<JobList />);
wrapper.find('JobList').setState({
jobs: mockResults,
itemCount: 6,
selected: mockResults.slice(1, 2),
});
wrapper.find('ToolbarDeleteButton').prop('onDelete')();
await waitForElement(
wrapper,
'Modal',
el => el.props().isOpen === true && el.props().title === 'Error!'
);
done(); done();
}); });

View File

@ -7,6 +7,7 @@ import { Card, PageSection, PageSectionVariants } from '@patternfly/react-core';
import { OrganizationsAPI } from '@api'; import { OrganizationsAPI } from '@api';
import AlertModal from '@components/AlertModal'; import AlertModal from '@components/AlertModal';
import DataListToolbar from '@components/DataListToolbar'; import DataListToolbar from '@components/DataListToolbar';
import ErrorDetail from '@components/ErrorDetail';
import PaginatedDataList, { import PaginatedDataList, {
ToolbarAddButton, ToolbarAddButton,
ToolbarDeleteButton, ToolbarDeleteButton,
@ -28,7 +29,7 @@ class OrganizationsList extends Component {
this.state = { this.state = {
hasContentLoading: true, hasContentLoading: true,
contentError: null, contentError: null,
hasDeletionError: false, deletionError: null,
organizations: [], organizations: [],
selected: [], selected: [],
itemCount: 0, itemCount: 0,
@ -71,17 +72,17 @@ class OrganizationsList extends Component {
} }
handleDeleteErrorClose() { handleDeleteErrorClose() {
this.setState({ hasDeletionError: false }); this.setState({ deletionError: null });
} }
async handleOrgDelete() { async handleOrgDelete() {
const { selected } = this.state; const { selected } = this.state;
this.setState({ hasContentLoading: true, hasDeletionError: false }); this.setState({ hasContentLoading: true });
try { try {
await Promise.all(selected.map(org => OrganizationsAPI.destroy(org.id))); await Promise.all(selected.map(org => OrganizationsAPI.destroy(org.id)));
} catch (err) { } catch (err) {
this.setState({ hasDeletionError: true }); this.setState({ deletionError: err });
} finally { } finally {
await this.loadOrganizations(); await this.loadOrganizations();
} }
@ -134,7 +135,7 @@ class OrganizationsList extends Component {
itemCount, itemCount,
contentError, contentError,
hasContentLoading, hasContentLoading,
hasDeletionError, deletionError,
selected, selected,
organizations, organizations,
} = this.state; } = this.state;
@ -212,12 +213,13 @@ class OrganizationsList extends Component {
</Card> </Card>
</PageSection> </PageSection>
<AlertModal <AlertModal
isOpen={hasDeletionError} isOpen={deletionError}
variant="danger" variant="danger"
title={i18n._(t`Error!`)} title={i18n._(t`Error!`)}
onClose={this.handleDeleteErrorClose} onClose={this.handleDeleteErrorClose}
> >
{i18n._(t`Failed to delete one or more organizations.`)} {i18n._(t`Failed to delete one or more organizations.`)}
<ErrorDetail error={deletionError} />
</AlertModal> </AlertModal>
</Fragment> </Fragment>
); );

View File

@ -15,6 +15,7 @@ import {
import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '@api'; import { JobTemplatesAPI, WorkflowJobTemplatesAPI } from '@api';
import AlertModal from '@components/AlertModal'; import AlertModal from '@components/AlertModal';
import DatalistToolbar from '@components/DataListToolbar'; import DatalistToolbar from '@components/DataListToolbar';
import ErrorDetail from '@components/ErrorDetail';
import PaginatedDataList, { import PaginatedDataList, {
ToolbarDeleteButton, ToolbarDeleteButton,
ToolbarAddButton, ToolbarAddButton,
@ -39,7 +40,7 @@ class TemplatesList extends Component {
this.state = { this.state = {
hasContentLoading: true, hasContentLoading: true,
contentError: null, contentError: null,
hasDeletionError: false, deletionError: null,
selected: [], selected: [],
templates: [], templates: [],
itemCount: 0, itemCount: 0,
@ -66,7 +67,7 @@ class TemplatesList extends Component {
} }
handleDeleteErrorClose() { handleDeleteErrorClose() {
this.setState({ hasDeletionError: false }); this.setState({ deletionError: null });
} }
handleSelectAll(isSelected) { handleSelectAll(isSelected) {
@ -92,7 +93,7 @@ class TemplatesList extends Component {
async handleTemplateDelete() { async handleTemplateDelete() {
const { selected, itemCount } = this.state; const { selected, itemCount } = this.state;
this.setState({ hasContentLoading: true, hasDeletionError: false }); this.setState({ hasContentLoading: true });
try { try {
await Promise.all( await Promise.all(
selected.map(({ type, id }) => { selected.map(({ type, id }) => {
@ -107,7 +108,7 @@ class TemplatesList extends Component {
); );
this.setState({ itemCount: itemCount - selected.length }); this.setState({ itemCount: itemCount - selected.length });
} catch (err) { } catch (err) {
this.setState({ hasDeletionError: true }); this.setState({ deletionError: err });
} finally { } finally {
await this.loadTemplates(); await this.loadTemplates();
} }
@ -159,7 +160,7 @@ class TemplatesList extends Component {
const { const {
contentError, contentError,
hasContentLoading, hasContentLoading,
hasDeletionError, deletionError,
templates, templates,
itemCount, itemCount,
selected, selected,
@ -287,12 +288,13 @@ class TemplatesList extends Component {
/> />
</Card> </Card>
<AlertModal <AlertModal
isOpen={hasDeletionError} isOpen={deletionError}
variant="danger" variant="danger"
title={i18n._(t`Error!`)} title={i18n._(t`Error!`)}
onClose={this.handleDeleteErrorClose} onClose={this.handleDeleteErrorClose}
> >
{i18n._(t`Failed to delete one or more template.`)} {i18n._(t`Failed to delete one or more template.`)}
<ErrorDetail error={deletionError} />
</AlertModal> </AlertModal>
</PageSection> </PageSection>
); );

View File

@ -199,7 +199,7 @@ describe('<TemplatesList />', () => {
expect(WorkflowJobTemplatesAPI.destroy).toHaveBeenCalledTimes(1); expect(WorkflowJobTemplatesAPI.destroy).toHaveBeenCalledTimes(1);
}); });
test('error is shown when template not successfully deleted from api', async () => { test('error is shown when template not successfully deleted from api', async done => {
JobTemplatesAPI.destroy.mockRejectedValue( JobTemplatesAPI.destroy.mockRejectedValue(
new Error({ new Error({
response: { response: {
@ -225,5 +225,7 @@ describe('<TemplatesList />', () => {
'Modal', 'Modal',
el => el.props().isOpen === true && el.props().title === 'Error!' el => el.props().isOpen === true && el.props().title === 'Error!'
); );
done();
}); });
}); });