diff --git a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx
index 51e62c6342..2705f1803c 100644
--- a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx
+++ b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx
@@ -1,18 +1,34 @@
-import React from 'react';
+import React, { useState } from 'react';
import { Link, withRouter } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { CardBody, Button } from '@patternfly/react-core';
import styled from 'styled-components';
+
+import AlertModal from '@components/AlertModal';
import { DetailList, Detail } from '@components/DetailList';
import { ChipGroup, Chip, CredentialChip } from '@components/Chip';
import { VariablesInput as _VariablesInput } from '@components/CodeMirrorInput';
+import ErrorDetail from '@components/ErrorDetail';
import { toTitleCase } from '@util/strings';
import { Job } from '../../../types';
+import {
+ JobsAPI,
+ ProjectUpdatesAPI,
+ SystemJobsAPI,
+ WorkflowJobsAPI,
+ InventoriesAPI,
+ AdHocCommandsAPI,
+} from '@api';
+import { JOB_TYPE_URL_SEGMENTS } from '../../../constants';
const ActionButtonWrapper = styled.div`
display: flex;
justify-content: flex-end;
+ margin-top: 20px;
+ & > :not(:first-child) {
+ margin-left: 20px;
+ }
`;
const VariablesInput = styled(_VariablesInput)`
@@ -29,7 +45,7 @@ const VERBOSITY = {
4: '4 (Connection Debug)',
};
-function JobDetail({ job, i18n }) {
+function JobDetail({ job, i18n, history }) {
const {
job_template: jobTemplate,
project,
@@ -38,7 +54,36 @@ function JobDetail({ job, i18n }) {
credentials,
labels,
} = job.summary_fields;
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+ const [errorMsg, setErrorMsg] = useState();
+ const deleteJob = async () => {
+ try {
+ switch (job.type) {
+ case 'project_update':
+ await ProjectUpdatesAPI.destroy(job.id);
+ break;
+ case 'system_job':
+ await SystemJobsAPI.destroy(job.id);
+ break;
+ case 'workflow_job':
+ await WorkflowJobsAPI.destroy(job.id);
+ break;
+ case 'ad_hoc_command':
+ await AdHocCommandsAPI.destroy(job.id);
+ break;
+ case 'inventory_update':
+ await InventoriesAPI.destroy(job.id);
+ break;
+ default:
+ await JobsAPI.destroy(job.id);
+ }
+ history.push('/jobs');
+ } catch (err) {
+ setErrorMsg(err);
+ setIsDeleteModalOpen(false);
+ }
+ };
return (
@@ -145,6 +190,13 @@ function JobDetail({ job, i18n }) {
/>
)}
+
+ {isDeleteModalOpen && (
+ setIsDeleteModalOpen(false)}
+ >
+ {i18n._(t`Are you sure you want to delete:`)}
+
+ {job.name}
+
+
+
+
+
+ )}
+ {errorMsg && (
+ setErrorMsg()}
+ title={i18n._(t`Job Delete Error`)}
+ >
+
+
+ )}
);
}
diff --git a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.test.jsx b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.test.jsx
index 161ec74d1f..939616ff3f 100644
--- a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.test.jsx
+++ b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.test.jsx
@@ -1,6 +1,11 @@
import React from 'react';
+import { act } from 'react-dom/test-utils';
import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import { sleep } from '@testUtils/testUtils';
import JobDetail from './JobDetail';
+import { JobsAPI, ProjectUpdatesAPI } from '@api';
+
+jest.mock('@api');
describe('', () => {
let job;
@@ -57,4 +62,63 @@ describe('', () => {
job.summary_fields.credentials[0]
);
});
+ test('should properly delete job', async () => {
+ job = {
+ name: 'Rage',
+ id: 1,
+ type: 'job',
+ summary_fields: {
+ job_template: { name: 'Spud' },
+ },
+ };
+ const wrapper = mountWithContexts();
+ wrapper
+ .find('button')
+ .at(0)
+ .simulate('click');
+ await sleep(1);
+ wrapper.update();
+ const modal = wrapper.find('Modal');
+ expect(modal.length).toBe(1);
+ modal.find('button[aria-label="Delete"]').simulate('click');
+ expect(JobsAPI.destroy).toHaveBeenCalledTimes(1);
+ });
+ // The test below is skipped until react can be upgraded to at least 16.9.0. An upgrade to
+ // react - router will likely be necessary also.
+ test.skip('should display error modal when a job does not delete properly', async () => {
+ job = {
+ name: 'Angry',
+ id: 'a',
+ type: 'project_update',
+ summary_fields: {
+ job_template: { name: 'Peanut' },
+ },
+ };
+ ProjectUpdatesAPI.destroy.mockRejectedValue(
+ new Error({
+ response: {
+ config: {
+ method: 'delete',
+ url: '/api/v2/project_updates/1',
+ },
+ data: 'An error occurred',
+ status: 404,
+ },
+ })
+ );
+ const wrapper = mountWithContexts();
+
+ wrapper
+ .find('button')
+ .at(0)
+ .simulate('click');
+ const modal = wrapper.find('Modal');
+ await act(async () => {
+ await modal.find('Button[variant="danger"]').prop('onClick')();
+ });
+ wrapper.update();
+
+ const errorModal = wrapper.find('ErrorDetail');
+ expect(errorModal.length).toBe(1);
+ });
});