diff --git a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx index 51e62c6342..b2bc05c4e7 100644 --- a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx +++ b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx @@ -1,18 +1,27 @@ -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 } 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 +38,7 @@ const VERBOSITY = { 4: '4 (Connection Debug)', }; -function JobDetail({ job, i18n }) { +function JobDetail({ job, i18n, history }) { const { job_template: jobTemplate, project, @@ -38,7 +47,22 @@ function JobDetail({ job, i18n }) { credentials, labels, } = job.summary_fields; + const [isDeleteModalOpen, setDeleteModal] = useState(false); + const [errorMsg, setErrorMsg] = useState(); + const deleteJob = async () => { + try { + if (job.type === 'job') { + await JobsAPI.destroy(job.id); + } else { + await ProjectUpdatesAPI.destroy(job.id); + } + history.push('/jobs'); + } catch (err) { + setErrorMsg(err); + setDeleteModal(false); + } + }; return ( @@ -145,6 +169,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..8f78ccb39c 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,10 @@ import React from 'react'; 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 +61,57 @@ describe('', () => { job.summary_fields.credentials[0] ); }); + test('should properly delete job', () => { + job = { + name: 'Rage', + id: 1, + type: 'job', + summary_fields: { + job_template: { name: 'Spud' }, + }, + }; + const wrapper = mountWithContexts(); + wrapper + .find('button') + .at(0) + .invoke('onClick')(); + const modal = wrapper.find('Modal'); + expect(modal.length).toBe(1); + modal.find('button[aria-label="delete"]').invoke('onClick')(); + expect(JobsAPI.destroy).toHaveBeenCalledTimes(1); + }); + + test('should display error modal when a job does not delete properly', async () => { + job = { + name: 'Angry', + id: 'a', + type: 'project_updates', + summary_fields: { + job_template: { name: 'Peanut' }, + }, + }; + const wrapper = mountWithContexts(); + wrapper + .find('button') + .at(0) + .invoke('onClick')(); + const modal = wrapper.find('Modal'); + ProjectUpdatesAPI.destroy.mockRejectedValue( + new Error({ + response: { + config: { + method: 'delete', + url: '/api/v2/project_updates/1', + }, + data: 'An error occurred', + status: 404, + }, + }) + ); + modal.find('button[aria-label="delete"]').invoke('onClick')(); + await sleep(1); + wrapper.update(); + const errorModal = wrapper.find('ErrorDetail__Expandable'); + expect(errorModal.length).toBe(1); + }); });