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 }) { /> )} + + + + + )} + {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); + }); });