From 199d25d1425f4a8e6a1255864b0c30cd2a7430a9 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Fri, 11 Dec 2020 15:06:23 -0500 Subject: [PATCH] Add ability to cancel jobs --- .../src/screens/Job/JobOutput/JobOutput.jsx | 126 +++++++++++++++--- .../Job/JobOutput/shared/OutputToolbar.jsx | 42 ++++-- 2 files changed, 134 insertions(+), 34 deletions(-) diff --git a/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx b/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx index 88e32be49fdf..c88c8710ea98 100644 --- a/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx +++ b/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx @@ -1,6 +1,6 @@ import React, { Component, Fragment } from 'react'; import { withRouter } from 'react-router-dom'; -import { I18n } from '@lingui/react'; +import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import styled from 'styled-components'; import { @@ -10,6 +10,7 @@ import { InfiniteLoader, List, } from 'react-virtualized'; +import { Button } from '@patternfly/react-core'; import Ansi from 'ansi-to-html'; import hasAnsi from 'has-ansi'; import { AllHtmlEntities } from 'html-entities'; @@ -225,6 +226,7 @@ class JobOutput extends Component { this.state = { contentError: null, deletionError: null, + cancelError: null, hasContentLoading: true, results: {}, currentlyLoading: [], @@ -232,6 +234,9 @@ class JobOutput extends Component { isHostModalOpen: false, hostEvent: {}, cssMap: {}, + jobStatus: props.job.status ?? 'waiting', + showCancelPrompt: false, + cancelInProgress: false, }; this.cache = new CellMeasurerCache({ @@ -242,6 +247,9 @@ class JobOutput extends Component { this._isMounted = false; this.loadJobEvents = this.loadJobEvents.bind(this); this.handleDeleteJob = this.handleDeleteJob.bind(this); + this.handleCancelOpen = this.handleCancelOpen.bind(this); + this.handleCancelConfirm = this.handleCancelConfirm.bind(this); + this.handleCancelClose = this.handleCancelClose.bind(this); this.rowRenderer = this.rowRenderer.bind(this); this.handleHostEventClick = this.handleHostEventClick.bind(this); this.handleHostModalClose = this.handleHostModalClose.bind(this); @@ -262,10 +270,18 @@ class JobOutput extends Component { this.loadJobEvents(); connectJobSocket(job, data => { - if (data.counter && data.counter > this.jobSocketCounter) { - this.jobSocketCounter = data.counter; - } else if (data.final_counter && data.unified_job_id === job.id) { - this.jobSocketCounter = data.final_counter; + if (data.group_name === 'job_events') { + if (data.counter && data.counter > this.jobSocketCounter) { + this.jobSocketCounter = data.counter; + } + } + if (data.group_name === 'jobs' && data.unified_job_id === job.id) { + if (data.final_counter) { + this.jobSocketCounter = data.final_counter; + } + if (data.status) { + this.setState({ jobStatus: data.status }); + } } }); this.interval = setInterval(() => this.monitorJobSocketCounter(), 5000); @@ -344,6 +360,26 @@ class JobOutput extends Component { } } + handleCancelOpen() { + this.setState({ showCancelPrompt: true }); + } + + handleCancelClose() { + this.setState({ showCancelPrompt: false }); + } + + async handleCancelConfirm() { + const { job, type } = this.props; + this.setState({ cancelInProgress: true }); + try { + await JobsAPI.cancel(job.id, type); + } catch (cancelError) { + this.setState({ cancelError }); + } finally { + this.setState({ showCancelPrompt: false, cancelInProgress: false }); + } + } + async handleDeleteJob() { const { job, history } = this.props; try { @@ -518,7 +554,7 @@ class JobOutput extends Component { } render() { - const { job } = this.props; + const { job, i18n } = this.props; const { contentError, @@ -528,6 +564,10 @@ class JobOutput extends Component { isHostModalOpen, remoteRowCount, cssMap, + jobStatus, + showCancelPrompt, + cancelError, + cancelInProgress, } = this.state; if (hasContentLoading) { @@ -553,7 +593,12 @@ class JobOutput extends Component {

{job.name}

- + + {showCancelPrompt && ( + + {i18n._(t`Cancel Job`)} + , + , + ]} + > + {i18n._( + t`Are you sure you want to submit the request to cancel this job?` + )} + + )} + {cancelError && ( + <> + this.setState({ cancelError: null })} + title={i18n._(t`Job Cancel Error`)} + label={i18n._(t`Job Cancel Error`)} + > + + + + )} {deletionError && ( <> - - {({ i18n }) => ( - this.setState({ deletionError: null })} - title={i18n._(t`Job Delete Error`)} - label={i18n._(t`Job Delete Error`)} - > - - - )} - + this.setState({ deletionError: null })} + title={i18n._(t`Job Delete Error`)} + label={i18n._(t`Job Delete Error`)} + > + + )} @@ -618,4 +702,4 @@ class JobOutput extends Component { } export { JobOutput as _JobOutput }; -export default withRouter(JobOutput); +export default withI18n()(withRouter(JobOutput)); diff --git a/awx/ui_next/src/screens/Job/JobOutput/shared/OutputToolbar.jsx b/awx/ui_next/src/screens/Job/JobOutput/shared/OutputToolbar.jsx index d98d2f72b2ce..868b6a1570a0 100644 --- a/awx/ui_next/src/screens/Job/JobOutput/shared/OutputToolbar.jsx +++ b/awx/ui_next/src/screens/Job/JobOutput/shared/OutputToolbar.jsx @@ -4,6 +4,7 @@ import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { shape, func } from 'prop-types'; import { + MinusCircleIcon, DownloadIcon, RocketIcon, TrashAltIcon, @@ -58,7 +59,7 @@ const OUTPUT_NO_COUNT_JOB_TYPES = [ 'inventory_update', ]; -const OutputToolbar = ({ i18n, job, onDelete }) => { +const OutputToolbar = ({ i18n, job, jobStatus, onDelete, onCancel }) => { const hideCounts = OUTPUT_NO_COUNT_JOB_TYPES.includes(job.type); const playCount = job?.playbook_counts?.play_count; @@ -148,19 +149,34 @@ const OutputToolbar = ({ i18n, job, onDelete }) => { )} + {job.summary_fields.user_capabilities.start && + ['pending', 'waiting', 'running'].includes(jobStatus) && ( + + + + )} - {job.summary_fields.user_capabilities.delete && ( - - - - - - )} + {job.summary_fields.user_capabilities.delete && + ['new', 'successful', 'failed', 'error', 'canceled'].includes( + jobStatus + ) && ( + + + + + + )} ); };