diff --git a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx
index 678b4c24bc..3037711e08 100644
--- a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx
+++ b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx
@@ -1,5 +1,5 @@
import 'styled-components/macro';
-import React, { useState } from 'react';
+import React, { useCallback, useState } from 'react';
import { Link, useHistory } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
@@ -25,7 +25,9 @@ import {
} from '../../../components/LaunchButton';
import StatusIcon from '../../../components/StatusIcon';
import ExecutionEnvironmentDetail from '../../../components/ExecutionEnvironmentDetail';
+import isJobRunning from '../../../util/jobs';
import { toTitleCase } from '../../../util/strings';
+import useRequest, { useDismissableError } from '../../../util/useRequest';
import { formatDateString } from '../../../util/dates';
import { Job } from '../../../types';
import {
@@ -58,6 +60,16 @@ const VERBOSITY = {
4: '4 (Connection Debug)',
};
+const getJobModel = type => {
+ if (type === 'ad_hoc_command') return AdHocCommandsAPI;
+ if (type === 'inventory_update') return InventoriesAPI;
+ if (type === 'project_update') return ProjectUpdatesAPI;
+ if (type === 'system_job') return SystemJobsAPI;
+ if (type === 'workflow_job') return WorkflowJobsAPI;
+
+ return JobsAPI;
+};
+
function JobDetail({ job, i18n }) {
const {
created_by,
@@ -77,6 +89,24 @@ function JobDetail({ job, i18n }) {
const [errorMsg, setErrorMsg] = useState();
const history = useHistory();
+ const [showCancelModal, setShowCancelModal] = useState(false);
+
+ const {
+ error: cancelError,
+ isLoading: isCancelling,
+ request: cancelJob,
+ } = useRequest(
+ useCallback(async () => {
+ await getJobModel(job.type).cancel(job.id, job.type);
+ }, [job.id, job.type]),
+ {}
+ );
+
+ const {
+ error: dismissableCancelError,
+ dismissError: dismissCancelError,
+ } = useDismissableError(cancelError);
+
const jobTypes = {
project_update: i18n._(t`Source Control Update`),
inventory_update: i18n._(t`Inventory Sync`),
@@ -91,25 +121,7 @@ function JobDetail({ job, i18n }) {
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);
- }
+ await getJobModel(job.type).destroy(job.id);
history.push('/jobs');
} catch (err) {
setErrorMsg(err);
@@ -410,16 +422,75 @@ function JobDetail({ job, i18n }) {
)}
))}
- {job.summary_fields.user_capabilities.delete && (
-
- {i18n._(t`Delete`)}
-
- )}
+ {isJobRunning(job.status) &&
+ job?.summary_fields?.user_capabilities?.start && (
+
+ )}
+ {!isJobRunning(job.status) &&
+ job?.summary_fields?.user_capabilities?.delete && (
+
+ {i18n._(t`Delete`)}
+
+ )}
+ {showCancelModal && isJobRunning(job.status) && (
+ setShowCancelModal(false)}
+ title={i18n._(t`Cancel Job`)}
+ label={i18n._(t`Cancel Job`)}
+ actions={[
+ ,
+ ,
+ ]}
+ >
+ {i18n._(
+ t`Are you sure you want to submit the request to cancel this job?`
+ )}
+
+ )}
+ {dismissableCancelError && (
+
+
+
+ )}
{errorMsg && (