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:
parent
1d05c21af4
commit
5549dac17d
@ -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,
|
||||
};
|
||||
|
10
awx/ui_next/src/api/models/AdHocCommands.js
Normal file
10
awx/ui_next/src/api/models/AdHocCommands.js
Normal 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;
|
10
awx/ui_next/src/api/models/InventoryUpdates.js
Normal file
10
awx/ui_next/src/api/models/InventoryUpdates.js
Normal 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;
|
10
awx/ui_next/src/api/models/ProjectUpdates.js
Normal file
10
awx/ui_next/src/api/models/ProjectUpdates.js
Normal 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;
|
10
awx/ui_next/src/api/models/SystemJobs.js
Normal file
10
awx/ui_next/src/api/models/SystemJobs.js
Normal 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;
|
10
awx/ui_next/src/api/models/WorkflowJobs.js
Normal file
10
awx/ui_next/src/api/models/WorkflowJobs.js
Normal 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;
|
@ -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>
|
||||
);
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user