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 InstanceGroups from './models/InstanceGroups';
import Inventories from './models/Inventories';
import InventoryUpdates from './models/InventoryUpdates';
import JobTemplates from './models/JobTemplates';
import Jobs from './models/Jobs';
import Labels from './models/Labels';
import Me from './models/Me';
import Organizations from './models/Organizations';
import Projects from './models/Projects';
import ProjectUpdates from './models/ProjectUpdates';
import Root from './models/Root';
import SystemJobs from './models/SystemJobs';
import Teams from './models/Teams';
import UnifiedJobTemplates from './models/UnifiedJobTemplates';
import UnifiedJobs from './models/UnifiedJobs';
import Users from './models/Users';
import WorkflowJobs from './models/WorkflowJobs';
import WorkflowJobTemplates from './models/WorkflowJobTemplates';
const AdHocCommandsAPI = new AdHocCommands();
const ConfigAPI = new Config();
const InstanceGroupsAPI = new InstanceGroups();
const InventoriesAPI = new Inventories();
const InventoryUpdatesAPI = new InventoryUpdates();
const JobTemplatesAPI = new JobTemplates();
const JobsAPI = new Jobs();
const LabelsAPI = new Labels();
const MeAPI = new Me();
const OrganizationsAPI = new Organizations();
const ProjectsAPI = new Projects();
const ProjectUpdatesAPI = new ProjectUpdates();
const RootAPI = new Root();
const SystemJobsAPI = new SystemJobs();
const TeamsAPI = new Teams();
const UnifiedJobTemplatesAPI = new UnifiedJobTemplates();
const UnifiedJobsAPI = new UnifiedJobs();
const UsersAPI = new Users();
const WorkflowJobsAPI = new WorkflowJobs();
const WorkflowJobTemplatesAPI = new WorkflowJobTemplates();
export {
AdHocCommandsAPI,
ConfigAPI,
InstanceGroupsAPI,
InventoriesAPI,
InventoryUpdatesAPI,
JobTemplatesAPI,
JobsAPI,
LabelsAPI,
MeAPI,
OrganizationsAPI,
ProjectsAPI,
ProjectUpdatesAPI,
RootAPI,
SystemJobsAPI,
TeamsAPI,
UnifiedJobTemplatesAPI,
UnifiedJobsAPI,
UsersAPI,
WorkflowJobsAPI,
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 { 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 DatalistToolbar from '@components/DataListToolbar';
import ErrorDetail from '@components/ErrorDetail';
import PaginatedDataList, {
ToolbarDeleteButton,
} from '@components/PaginatedDataList';
@ -27,8 +36,8 @@ class JobList extends Component {
this.state = {
hasContentLoading: true,
deletionError: null,
contentError: null,
deletionError: false,
selected: [],
jobs: [],
itemCount: 0,
@ -36,7 +45,7 @@ class JobList extends Component {
this.loadJobs = this.loadJobs.bind(this);
this.handleSelectAll = this.handleSelectAll.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);
}
@ -52,7 +61,7 @@ class JobList extends Component {
}
handleDeleteErrorClose() {
this.setState({ deletionError: false });
this.setState({ deletionError: null });
}
handleSelectAll(isSelected) {
@ -70,13 +79,41 @@ class JobList extends Component {
}
}
async handleDelete() {
const { selected } = this.state;
this.setState({ hasContentLoading: true, deletionError: false });
async handleJobDelete() {
const { selected, itemCount } = this.state;
this.setState({ hasContentLoading: true });
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) {
this.setState({ deletionError: true });
this.setState({ deletionError: err });
} finally {
await this.loadJobs();
}
@ -150,7 +187,7 @@ class JobList extends Component {
additionalControls={[
<ToolbarDeleteButton
key="delete"
onDelete={this.handleDelete}
onDelete={this.handleJobDelete}
itemsToDelete={selected}
itemName={itemName}
/>,
@ -176,6 +213,7 @@ class JobList extends Component {
onClose={this.handleDeleteErrorClose}
>
{i18n._(t`Failed to delete one or more jobs.`)}
<ErrorDetail error={deletionError} />
</AlertModal>
</PageSection>
);

View File

@ -1,7 +1,15 @@
import React from 'react';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import { UnifiedJobsAPI } from '@api';
import {
AdHocCommandsAPI,
InventoryUpdatesAPI,
JobsAPI,
ProjectUpdatesAPI,
SystemJobsAPI,
UnifiedJobsAPI,
WorkflowJobsAPI,
} from '@api';
import JobList from './JobList';
jest.mock('@api');
@ -11,7 +19,7 @@ const mockResults = [
id: 1,
url: '/api/v2/project_updates/1',
name: 'job 1',
type: 'project update',
type: 'project_update',
summary_fields: {
user_capabilities: {
delete: true,
@ -31,9 +39,42 @@ const mockResults = [
},
{
id: 3,
url: '/api/v2/jobs/3',
url: '/api/v2/inventory_updates/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: {
user_capabilities: {
delete: true,
@ -52,7 +93,7 @@ describe('<JobList />', () => {
await waitForElement(
wrapper,
'JobList',
el => el.state('jobs').length === 3
el => el.state('jobs').length === 6
);
done();
@ -61,7 +102,7 @@ describe('<JobList />', () => {
test('select makes expected state updates', async done => {
const [mockItem] = mockResults;
const wrapper = mountWithContexts(<JobList />);
await waitForElement(wrapper, 'JobListItem', el => el.length === 3);
await waitForElement(wrapper, 'JobListItem', el => el.length === 6);
wrapper
.find('JobListItem')
@ -79,20 +120,59 @@ describe('<JobList />', () => {
});
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 />);
await waitForElement(wrapper, 'JobListItem', el => el.length === 3);
await waitForElement(wrapper, 'JobListItem', el => el.length === 6);
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);
expect(wrapper.find('JobList').state('selected').length).toEqual(0);
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')();
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();
});

View File

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

View File

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

View File

@ -199,7 +199,7 @@ describe('<TemplatesList />', () => {
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(
new Error({
response: {
@ -225,5 +225,7 @@ describe('<TemplatesList />', () => {
'Modal',
el => el.props().isOpen === true && el.props().title === 'Error!'
);
done();
});
});