mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 10:30:03 -03:30
Merge pull request #9659 from jakemcdermott/fix-7657
Support job cancellation through details panel SUMMARY for #7657 edit: also addresses #8838 cc @nixocio Reviewed-by: Kersom <None> Reviewed-by: Jake McDermott <yo@jakemcdermott.me>
This commit is contained in:
commit
a7992d06e3
@ -1,12 +0,0 @@
|
||||
const RelaunchMixin = parent =>
|
||||
class extends parent {
|
||||
relaunch(id, data) {
|
||||
return this.http.post(`${this.baseUrl}${id}/relaunch/`, data);
|
||||
}
|
||||
|
||||
readRelaunch(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/relaunch/`);
|
||||
}
|
||||
};
|
||||
|
||||
export default RelaunchMixin;
|
||||
48
awx/ui_next/src/api/mixins/Runnable.mixin.js
Normal file
48
awx/ui_next/src/api/mixins/Runnable.mixin.js
Normal file
@ -0,0 +1,48 @@
|
||||
const Runnable = parent =>
|
||||
class extends parent {
|
||||
jobEventSlug = '/events/';
|
||||
|
||||
cancel(id) {
|
||||
const endpoint = `${this.baseUrl}${id}/cancel/`;
|
||||
|
||||
return this.http.post(endpoint);
|
||||
}
|
||||
|
||||
launchUpdate(id, data) {
|
||||
const endpoint = `${this.baseUrl}${id}/update/`;
|
||||
|
||||
return this.http.post(endpoint, data);
|
||||
}
|
||||
|
||||
readLaunchUpdate(id) {
|
||||
const endpoint = `${this.baseUrl}${id}/update/`;
|
||||
|
||||
return this.http.get(endpoint);
|
||||
}
|
||||
|
||||
readEvents(id, params = {}) {
|
||||
const endpoint = `${this.baseUrl}${id}${this.jobEventSlug}`;
|
||||
|
||||
return this.http.get(endpoint, { params });
|
||||
}
|
||||
|
||||
readEventOptions(id) {
|
||||
const endpoint = `${this.baseUrl}${id}${this.jobEventSlug}`;
|
||||
|
||||
return this.http.options(endpoint);
|
||||
}
|
||||
|
||||
readRelaunch(id) {
|
||||
const endpoint = `${this.baseUrl}${id}/relaunch/`;
|
||||
|
||||
return this.http.get(endpoint);
|
||||
}
|
||||
|
||||
relaunch(id, data) {
|
||||
const endpoint = `${this.baseUrl}${id}/relaunch/`;
|
||||
|
||||
return this.http.post(endpoint, data);
|
||||
}
|
||||
};
|
||||
|
||||
export default Runnable;
|
||||
@ -1,11 +1,15 @@
|
||||
import Base from '../Base';
|
||||
import RelaunchMixin from '../mixins/Relaunch.mixin';
|
||||
import RunnableMixin from '../mixins/Runnable.mixin';
|
||||
|
||||
class AdHocCommands extends RelaunchMixin(Base) {
|
||||
class AdHocCommands extends RunnableMixin(Base) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/ad_hoc_commands/';
|
||||
}
|
||||
|
||||
readCredentials(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/credentials/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default AdHocCommands;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import Base from '../Base';
|
||||
import LaunchUpdateMixin from '../mixins/LaunchUpdate.mixin';
|
||||
import RunnableMixin from '../mixins/Runnable.mixin';
|
||||
|
||||
class InventoryUpdates extends LaunchUpdateMixin(Base) {
|
||||
class InventoryUpdates extends RunnableMixin(Base) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/inventory_updates/';
|
||||
@ -11,5 +11,9 @@ class InventoryUpdates extends LaunchUpdateMixin(Base) {
|
||||
createSyncCancel(sourceId) {
|
||||
return this.http.post(`${this.baseUrl}${sourceId}/cancel/`);
|
||||
}
|
||||
|
||||
readCredentials(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/credentials/`);
|
||||
}
|
||||
}
|
||||
export default InventoryUpdates;
|
||||
|
||||
@ -1,67 +1,23 @@
|
||||
import Base from '../Base';
|
||||
import RelaunchMixin from '../mixins/Relaunch.mixin';
|
||||
import RunnableMixin from '../mixins/Runnable.mixin';
|
||||
|
||||
const getBaseURL = type => {
|
||||
switch (type) {
|
||||
case 'playbook':
|
||||
case 'job':
|
||||
return '/jobs/';
|
||||
case 'project':
|
||||
case 'project_update':
|
||||
return '/project_updates/';
|
||||
case 'management':
|
||||
case 'management_job':
|
||||
return '/system_jobs/';
|
||||
case 'inventory':
|
||||
case 'inventory_update':
|
||||
return '/inventory_updates/';
|
||||
case 'command':
|
||||
case 'ad_hoc_command':
|
||||
return '/ad_hoc_commands/';
|
||||
case 'workflow':
|
||||
case 'workflow_job':
|
||||
return '/workflow_jobs/';
|
||||
default:
|
||||
throw new Error('Unable to find matching job type');
|
||||
}
|
||||
};
|
||||
|
||||
class Jobs extends RelaunchMixin(Base) {
|
||||
class Jobs extends RunnableMixin(Base) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/jobs/';
|
||||
this.jobEventSlug = '/job_events/';
|
||||
}
|
||||
|
||||
cancel(id, type) {
|
||||
return this.http.post(`/api/v2${getBaseURL(type)}${id}/cancel/`);
|
||||
cancel(id) {
|
||||
return this.http.post(`${this.baseUrl}${id}/cancel/`);
|
||||
}
|
||||
|
||||
readCredentials(id, type) {
|
||||
return this.http.get(`/api/v2${getBaseURL(type)}${id}/credentials/`);
|
||||
readCredentials(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/credentials/`);
|
||||
}
|
||||
|
||||
readDetail(id, type) {
|
||||
return this.http.get(`/api/v2${getBaseURL(type)}${id}/`);
|
||||
}
|
||||
|
||||
readEvents(id, type = 'playbook', params = {}) {
|
||||
let endpoint;
|
||||
if (type === 'playbook') {
|
||||
endpoint = `/api/v2${getBaseURL(type)}${id}/job_events/`;
|
||||
} else {
|
||||
endpoint = `/api/v2${getBaseURL(type)}${id}/events/`;
|
||||
}
|
||||
return this.http.get(endpoint, { params });
|
||||
}
|
||||
|
||||
readEventOptions(id, type = 'playbook') {
|
||||
let endpoint;
|
||||
if (type === 'playbook') {
|
||||
endpoint = `/api/v2${getBaseURL(type)}${id}/job_events/`;
|
||||
} else {
|
||||
endpoint = `/api/v2${getBaseURL(type)}${id}/events/`;
|
||||
}
|
||||
return this.http.options(endpoint);
|
||||
readDetail(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
import Base from '../Base';
|
||||
import RunnableMixin from '../mixins/Runnable.mixin';
|
||||
|
||||
class ProjectUpdates extends Base {
|
||||
class ProjectUpdates extends RunnableMixin(Base) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/project_updates/';
|
||||
}
|
||||
|
||||
readCredentials(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/credentials/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default ProjectUpdates;
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class SystemJobs extends Base {
|
||||
import RunnableMixin from '../mixins/Runnable.mixin';
|
||||
|
||||
class SystemJobs extends RunnableMixin(Base) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/system_jobs/';
|
||||
}
|
||||
|
||||
readCredentials(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/credentials/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default SystemJobs;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import Base from '../Base';
|
||||
import RelaunchMixin from '../mixins/Relaunch.mixin';
|
||||
import RunnableMixin from '../mixins/Runnable.mixin';
|
||||
|
||||
class WorkflowJobs extends RelaunchMixin(Base) {
|
||||
class WorkflowJobs extends RunnableMixin(Base) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/workflow_jobs/';
|
||||
@ -10,6 +10,10 @@ class WorkflowJobs extends RelaunchMixin(Base) {
|
||||
readNodes(id, params) {
|
||||
return this.http.get(`${this.baseUrl}${id}/workflow_nodes/`, { params });
|
||||
}
|
||||
|
||||
readCredentials(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/credentials/`);
|
||||
}
|
||||
}
|
||||
|
||||
export default WorkflowJobs;
|
||||
|
||||
@ -13,20 +13,12 @@ import useRequest, {
|
||||
useDeleteItems,
|
||||
useDismissableError,
|
||||
} from '../../util/useRequest';
|
||||
import isJobRunning from '../../util/jobs';
|
||||
import { isJobRunning, getJobModel } from '../../util/jobs';
|
||||
import { getQSConfig, parseQueryString } from '../../util/qs';
|
||||
import JobListItem from './JobListItem';
|
||||
import JobListCancelButton from './JobListCancelButton';
|
||||
import useWsJobs from './useWsJobs';
|
||||
import {
|
||||
AdHocCommandsAPI,
|
||||
InventoryUpdatesAPI,
|
||||
JobsAPI,
|
||||
ProjectUpdatesAPI,
|
||||
SystemJobsAPI,
|
||||
UnifiedJobsAPI,
|
||||
WorkflowJobsAPI,
|
||||
} from '../../api';
|
||||
import { UnifiedJobsAPI } from '../../api';
|
||||
|
||||
function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
||||
const qsConfig = getQSConfig(
|
||||
@ -104,7 +96,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
||||
return Promise.all(
|
||||
selected.map(job => {
|
||||
if (isJobRunning(job.status)) {
|
||||
return JobsAPI.cancel(job.id, job.type);
|
||||
return getJobModel(job.type).cancel(job.id);
|
||||
}
|
||||
return Promise.resolve();
|
||||
})
|
||||
@ -127,22 +119,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
||||
useCallback(() => {
|
||||
return Promise.all(
|
||||
selected.map(({ type, id }) => {
|
||||
switch (type) {
|
||||
case 'job':
|
||||
return JobsAPI.destroy(id);
|
||||
case 'ad_hoc_command':
|
||||
return AdHocCommandsAPI.destroy(id);
|
||||
case 'system_job':
|
||||
return SystemJobsAPI.destroy(id);
|
||||
case 'project_update':
|
||||
return ProjectUpdatesAPI.destroy(id);
|
||||
case 'inventory_update':
|
||||
return InventoryUpdatesAPI.destroy(id);
|
||||
case 'workflow_job':
|
||||
return WorkflowJobsAPI.destroy(id);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
return getJobModel(type).destroy(id);
|
||||
})
|
||||
);
|
||||
}, [selected]),
|
||||
|
||||
@ -319,13 +319,12 @@ describe('<JobList />', () => {
|
||||
wrapper.find('JobListCancelButton').invoke('onCancel')();
|
||||
});
|
||||
|
||||
expect(JobsAPI.cancel).toHaveBeenCalledTimes(6);
|
||||
expect(JobsAPI.cancel).toHaveBeenCalledWith(1, 'project_update');
|
||||
expect(JobsAPI.cancel).toHaveBeenCalledWith(2, 'job');
|
||||
expect(JobsAPI.cancel).toHaveBeenCalledWith(3, 'inventory_update');
|
||||
expect(JobsAPI.cancel).toHaveBeenCalledWith(4, 'workflow_job');
|
||||
expect(JobsAPI.cancel).toHaveBeenCalledWith(5, 'system_job');
|
||||
expect(JobsAPI.cancel).toHaveBeenCalledWith(6, 'ad_hoc_command');
|
||||
expect(ProjectUpdatesAPI.cancel).toHaveBeenCalledWith(1);
|
||||
expect(JobsAPI.cancel).toHaveBeenCalledWith(2);
|
||||
expect(InventoryUpdatesAPI.cancel).toHaveBeenCalledWith(3);
|
||||
expect(WorkflowJobsAPI.cancel).toHaveBeenCalledWith(4);
|
||||
expect(SystemJobsAPI.cancel).toHaveBeenCalledWith(5);
|
||||
expect(AdHocCommandsAPI.cancel).toHaveBeenCalledWith(6);
|
||||
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
@ -4,7 +4,7 @@ import { t } from '@lingui/macro';
|
||||
import { arrayOf, func } from 'prop-types';
|
||||
import { Button, DropdownItem, Tooltip } from '@patternfly/react-core';
|
||||
import { KebabifiedContext } from '../../contexts/Kebabified';
|
||||
import isJobRunning from '../../util/jobs';
|
||||
import { isJobRunning } from '../../util/jobs';
|
||||
import AlertModal from '../AlertModal';
|
||||
import { Job } from '../../types';
|
||||
|
||||
|
||||
@ -12,20 +12,32 @@ import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||
import { Card, PageSection } from '@patternfly/react-core';
|
||||
import { JobsAPI } from '../../api';
|
||||
import ContentError from '../../components/ContentError';
|
||||
import ContentLoading from '../../components/ContentLoading';
|
||||
import RoutedTabs from '../../components/RoutedTabs';
|
||||
import useRequest from '../../util/useRequest';
|
||||
import { getJobModel } from '../../util/jobs';
|
||||
import JobDetail from './JobDetail';
|
||||
import JobOutput from './JobOutput';
|
||||
import { WorkflowOutput } from './WorkflowOutput';
|
||||
import useWsJob from './useWsJob';
|
||||
|
||||
// maps the displayed url segments to actual api types
|
||||
export const JOB_URL_SEGMENT_MAP = {
|
||||
playbook: 'job',
|
||||
project: 'project_update',
|
||||
management: 'system_job',
|
||||
inventory: 'inventory_update',
|
||||
command: 'ad_hoc_command',
|
||||
workflow: 'workflow_job',
|
||||
};
|
||||
|
||||
function Job({ i18n, setBreadcrumb }) {
|
||||
const { id, type } = useParams();
|
||||
const { id, typeSegment } = useParams();
|
||||
const match = useRouteMatch();
|
||||
|
||||
const type = JOB_URL_SEGMENT_MAP[typeSegment];
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
error,
|
||||
@ -34,12 +46,11 @@ function Job({ i18n, setBreadcrumb }) {
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
let eventOptions = {};
|
||||
const { data: jobDetailData } = await JobsAPI.readDetail(id, type);
|
||||
if (jobDetailData.type !== 'workflow_job') {
|
||||
const { data: jobEventOptions } = await JobsAPI.readEventOptions(
|
||||
id,
|
||||
const { data: jobDetailData } = await getJobModel(type).readDetail(id);
|
||||
if (type !== 'workflow_job') {
|
||||
const { data: jobEventOptions } = await getJobModel(
|
||||
type
|
||||
);
|
||||
).readEventOptions(id);
|
||||
eventOptions = jobEventOptions;
|
||||
}
|
||||
if (
|
||||
@ -49,7 +60,7 @@ function Job({ i18n, setBreadcrumb }) {
|
||||
) {
|
||||
const {
|
||||
data: { results },
|
||||
} = await JobsAPI.readCredentials(jobDetailData.id, type);
|
||||
} = await getJobModel(type).readCredentials(jobDetailData.id);
|
||||
|
||||
jobDetailData.summary_fields.credentials = results;
|
||||
}
|
||||
@ -125,37 +136,37 @@ function Job({ i18n, setBreadcrumb }) {
|
||||
<Card>
|
||||
<RoutedTabs tabsArray={tabsArray} />
|
||||
<Switch>
|
||||
<Redirect from="/jobs/:type/:id" to="/jobs/:type/:id/output" exact />
|
||||
{job &&
|
||||
job.type === 'workflow_job' && [
|
||||
<Route key="workflow-details" path="/jobs/workflow/:id/details">
|
||||
<JobDetail type={match.params.type} job={job} />
|
||||
</Route>,
|
||||
<Route key="workflow-output" path="/jobs/workflow/:id/output">
|
||||
<Redirect
|
||||
from="/jobs/:typeSegment/:id"
|
||||
to="/jobs/:typeSegment/:id/output"
|
||||
exact
|
||||
/>
|
||||
{job && [
|
||||
<Route
|
||||
key={job.type === 'workflow_job' ? 'workflow-details' : 'details'}
|
||||
path="/jobs/:typeSegment/:id/details"
|
||||
>
|
||||
<JobDetail job={job} />
|
||||
</Route>,
|
||||
<Route key="output" path="/jobs/:typeSegment/:id/output">
|
||||
{job.type === 'workflow_job' ? (
|
||||
<WorkflowOutput job={job} />
|
||||
</Route>,
|
||||
]}
|
||||
{job &&
|
||||
job.type !== 'workflow_job' && [
|
||||
<Route key="details" path="/jobs/:type/:id/details">
|
||||
<JobDetail type={type} job={job} />
|
||||
</Route>,
|
||||
<Route key="output" path="/jobs/:type/:id/output">
|
||||
) : (
|
||||
<JobOutput
|
||||
type={type}
|
||||
job={job}
|
||||
eventRelatedSearchableKeys={eventRelatedSearchableKeys}
|
||||
eventSearchableKeys={eventSearchableKeys}
|
||||
/>
|
||||
</Route>,
|
||||
<Route key="not-found" path="*">
|
||||
<ContentError isNotFound>
|
||||
<Link to={`/jobs/${type}/${id}/details`}>
|
||||
{i18n._(t`View Job Details`)}
|
||||
</Link>
|
||||
</ContentError>
|
||||
</Route>,
|
||||
]}
|
||||
)}
|
||||
</Route>,
|
||||
<Route key="not-found" path="*">
|
||||
<ContentError isNotFound>
|
||||
<Link to={`/jobs/${typeSegment}/${id}/details`}>
|
||||
{i18n._(t`View Job Details`)}
|
||||
</Link>
|
||||
</ContentError>
|
||||
</Route>,
|
||||
]}
|
||||
</Switch>
|
||||
</Card>
|
||||
</PageSection>
|
||||
|
||||
@ -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,17 +25,11 @@ import {
|
||||
} from '../../../components/LaunchButton';
|
||||
import StatusIcon from '../../../components/StatusIcon';
|
||||
import ExecutionEnvironmentDetail from '../../../components/ExecutionEnvironmentDetail';
|
||||
import { getJobModel, 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 {
|
||||
JobsAPI,
|
||||
ProjectUpdatesAPI,
|
||||
SystemJobsAPI,
|
||||
WorkflowJobsAPI,
|
||||
InventoriesAPI,
|
||||
AdHocCommandsAPI,
|
||||
} from '../../../api';
|
||||
|
||||
const VariablesInput = styled(_VariablesInput)`
|
||||
.pf-c-form__label {
|
||||
@ -77,6 +71,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 +103,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 +404,75 @@ function JobDetail({ job, i18n }) {
|
||||
)}
|
||||
</LaunchButton>
|
||||
))}
|
||||
{job.summary_fields.user_capabilities.delete && (
|
||||
<DeleteButton
|
||||
name={job.name}
|
||||
modalTitle={i18n._(t`Delete Job`)}
|
||||
onConfirm={deleteJob}
|
||||
>
|
||||
{i18n._(t`Delete`)}
|
||||
</DeleteButton>
|
||||
)}
|
||||
{isJobRunning(job.status) &&
|
||||
job?.summary_fields?.user_capabilities?.start && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
aria-label={i18n._(t`Cancel`)}
|
||||
isDisabled={isCancelling}
|
||||
onClick={() => setShowCancelModal(true)}
|
||||
ouiaId="job-detail-cancel-button"
|
||||
>
|
||||
{i18n._(t`Cancel`)}
|
||||
</Button>
|
||||
)}
|
||||
{!isJobRunning(job.status) &&
|
||||
job?.summary_fields?.user_capabilities?.delete && (
|
||||
<DeleteButton
|
||||
name={job.name}
|
||||
modalTitle={i18n._(t`Delete Job`)}
|
||||
onConfirm={deleteJob}
|
||||
ouiaId="job-detail-delete-button"
|
||||
>
|
||||
{i18n._(t`Delete`)}
|
||||
</DeleteButton>
|
||||
)}
|
||||
</CardActionsRow>
|
||||
{showCancelModal && isJobRunning(job.status) && (
|
||||
<AlertModal
|
||||
isOpen={showCancelModal}
|
||||
variant="danger"
|
||||
onClose={() => setShowCancelModal(false)}
|
||||
title={i18n._(t`Cancel Job`)}
|
||||
label={i18n._(t`Cancel Job`)}
|
||||
actions={[
|
||||
<Button
|
||||
id="cancel-job-confirm-button"
|
||||
key="delete"
|
||||
variant="danger"
|
||||
isDisabled={isCancelling}
|
||||
aria-label={i18n._(t`Cancel job`)}
|
||||
onClick={cancelJob}
|
||||
>
|
||||
{i18n._(t`Cancel job`)}
|
||||
</Button>,
|
||||
<Button
|
||||
id="cancel-job-return-button"
|
||||
key="cancel"
|
||||
variant="secondary"
|
||||
aria-label={i18n._(t`Return`)}
|
||||
onClick={() => setShowCancelModal(false)}
|
||||
>
|
||||
{i18n._(t`Return`)}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{i18n._(
|
||||
t`Are you sure you want to submit the request to cancel this job?`
|
||||
)}
|
||||
</AlertModal>
|
||||
)}
|
||||
{dismissableCancelError && (
|
||||
<AlertModal
|
||||
isOpen={dismissableCancelError}
|
||||
variant="danger"
|
||||
onClose={dismissCancelError}
|
||||
title={i18n._(t`Job Cancel Error`)}
|
||||
label={i18n._(t`Job Cancel Error`)}
|
||||
>
|
||||
<ErrorDetail error={dismissableCancelError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
{errorMsg && (
|
||||
<AlertModal
|
||||
isOpen={errorMsg}
|
||||
|
||||
@ -37,7 +37,7 @@ import PageControls from './PageControls';
|
||||
import HostEventModal from './HostEventModal';
|
||||
import { HostStatusBar, OutputToolbar } from './shared';
|
||||
import getRowRangePageSize from './shared/jobOutputUtils';
|
||||
import isJobRunning from '../../../util/jobs';
|
||||
import { getJobModel, isJobRunning } from '../../../util/jobs';
|
||||
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||
import {
|
||||
encodeNonDefaultQueryString,
|
||||
@ -47,14 +47,6 @@ import {
|
||||
removeParams,
|
||||
getQSConfig,
|
||||
} from '../../../util/qs';
|
||||
import {
|
||||
JobsAPI,
|
||||
ProjectUpdatesAPI,
|
||||
SystemJobsAPI,
|
||||
WorkflowJobsAPI,
|
||||
InventoriesAPI,
|
||||
AdHocCommandsAPI,
|
||||
} from '../../../api';
|
||||
|
||||
const QS_CONFIG = getQSConfig('job_output', {
|
||||
order_by: 'start_line',
|
||||
@ -280,12 +272,7 @@ const cache = new CellMeasurerCache({
|
||||
defaultHeight: 25,
|
||||
});
|
||||
|
||||
function JobOutput({
|
||||
job,
|
||||
type,
|
||||
eventRelatedSearchableKeys,
|
||||
eventSearchableKeys,
|
||||
}) {
|
||||
function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
||||
const location = useLocation();
|
||||
const listRef = useRef(null);
|
||||
const isMounted = useRef(false);
|
||||
@ -348,8 +335,8 @@ function JobOutput({
|
||||
request: cancelJob,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
await JobsAPI.cancel(job.id, type);
|
||||
}, [job.id, type]),
|
||||
await getJobModel(job.type).cancel(job.id);
|
||||
}, [job.id, job.type]),
|
||||
{}
|
||||
);
|
||||
|
||||
@ -364,27 +351,10 @@ function JobOutput({
|
||||
error: deleteError,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
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');
|
||||
}, [job, history])
|
||||
}, [job.type, job.id, history])
|
||||
);
|
||||
|
||||
const {
|
||||
@ -417,7 +387,7 @@ function JobOutput({
|
||||
try {
|
||||
const {
|
||||
data: { results: fetchedEvents = [], count },
|
||||
} = await JobsAPI.readEvents(job.id, type, {
|
||||
} = await getJobModel(job.type).readEvents(job.id, {
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
...parseQueryString(QS_CONFIG, location.search),
|
||||
@ -557,31 +527,33 @@ function JobOutput({
|
||||
...parseQueryString(QS_CONFIG, location.search),
|
||||
};
|
||||
|
||||
return JobsAPI.readEvents(job.id, type, params).then(response => {
|
||||
if (isMounted.current) {
|
||||
const newResults = {};
|
||||
let newResultsCssMap = {};
|
||||
response.data.results.forEach((jobEvent, index) => {
|
||||
newResults[firstIndex + index] = jobEvent;
|
||||
const { lineCssMap } = getLineTextHtml(jobEvent);
|
||||
newResultsCssMap = { ...newResultsCssMap, ...lineCssMap };
|
||||
});
|
||||
setResults(prevResults => ({
|
||||
...prevResults,
|
||||
...newResults,
|
||||
}));
|
||||
setCssMap(prevCssMap => ({
|
||||
...prevCssMap,
|
||||
...newResultsCssMap,
|
||||
}));
|
||||
setCurrentlyLoading(prevCurrentlyLoading =>
|
||||
prevCurrentlyLoading.filter(n => !loadRange.includes(n))
|
||||
);
|
||||
loadRange.forEach(n => {
|
||||
cache.clear(n);
|
||||
});
|
||||
}
|
||||
});
|
||||
return getJobModel(job.type)
|
||||
.readEvents(job.id, params)
|
||||
.then(response => {
|
||||
if (isMounted.current) {
|
||||
const newResults = {};
|
||||
let newResultsCssMap = {};
|
||||
response.data.results.forEach((jobEvent, index) => {
|
||||
newResults[firstIndex + index] = jobEvent;
|
||||
const { lineCssMap } = getLineTextHtml(jobEvent);
|
||||
newResultsCssMap = { ...newResultsCssMap, ...lineCssMap };
|
||||
});
|
||||
setResults(prevResults => ({
|
||||
...prevResults,
|
||||
...newResults,
|
||||
}));
|
||||
setCssMap(prevCssMap => ({
|
||||
...prevCssMap,
|
||||
...newResultsCssMap,
|
||||
}));
|
||||
setCurrentlyLoading(prevCurrentlyLoading =>
|
||||
prevCurrentlyLoading.filter(n => !loadRange.includes(n))
|
||||
);
|
||||
loadRange.forEach(n => {
|
||||
cache.clear(n);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const scrollToRow = rowIndex => {
|
||||
|
||||
@ -278,7 +278,7 @@ describe('<JobOutput />', () => {
|
||||
wrapper.find(searchBtn).simulate('click');
|
||||
});
|
||||
wrapper.update();
|
||||
expect(JobsAPI.readEvents).toHaveBeenCalledWith(2, undefined, {
|
||||
expect(JobsAPI.readEvents).toHaveBeenCalledWith(2, {
|
||||
order_by: 'start_line',
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
|
||||
@ -55,8 +55,8 @@ function JobTypeRedirect({ id, path, view, i18n }) {
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
const type = JOB_TYPE_URL_SEGMENTS[job.type];
|
||||
return <Redirect from={path} to={`/jobs/${type}/${job.id}/${view}`} />;
|
||||
const typeSegment = JOB_TYPE_URL_SEGMENTS[job.type];
|
||||
return <Redirect from={path} to={`/jobs/${typeSegment}/${job.id}/${view}`} />;
|
||||
}
|
||||
|
||||
JobTypeRedirect.defaultProps = {
|
||||
|
||||
@ -21,12 +21,12 @@ function Jobs({ i18n }) {
|
||||
return;
|
||||
}
|
||||
|
||||
const type = JOB_TYPE_URL_SEGMENTS[job.type];
|
||||
const typeSegment = JOB_TYPE_URL_SEGMENTS[job.type];
|
||||
setBreadcrumbConfig({
|
||||
'/jobs': i18n._(t`Jobs`),
|
||||
[`/jobs/${type}/${job.id}`]: `${job.name}`,
|
||||
[`/jobs/${type}/${job.id}/output`]: i18n._(t`Output`),
|
||||
[`/jobs/${type}/${job.id}/details`]: i18n._(t`Details`),
|
||||
[`/jobs/${typeSegment}/${job.id}`]: `${job.name}`,
|
||||
[`/jobs/${typeSegment}/${job.id}/output`]: i18n._(t`Output`),
|
||||
[`/jobs/${typeSegment}/${job.id}/details`]: i18n._(t`Details`),
|
||||
});
|
||||
},
|
||||
[i18n]
|
||||
@ -53,7 +53,7 @@ function Jobs({ i18n }) {
|
||||
<Route path={`${match.path}/:id/output`}>
|
||||
<TypeRedirect view="output" />
|
||||
</Route>
|
||||
<Route path={`${match.path}/:type/:id`}>
|
||||
<Route path={`${match.path}/:typeSegment/:id`}>
|
||||
<Job setBreadcrumb={buildBreadcrumbConfig} />
|
||||
</Route>
|
||||
<Route path={`${match.path}/:id`}>
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import useWebsocket from '../../util/useWebsocket';
|
||||
import { JobsAPI } from '../../api';
|
||||
import { getJobModel } from '../../util/jobs';
|
||||
|
||||
export default function useWsJob(initialJob) {
|
||||
const { type } = useParams();
|
||||
const [job, setJob] = useState(initialJob);
|
||||
const lastMessage = useWebsocket({
|
||||
jobs: ['status_changed'],
|
||||
@ -18,7 +16,7 @@ export default function useWsJob(initialJob) {
|
||||
useEffect(
|
||||
function parseWsMessage() {
|
||||
async function fetchJob() {
|
||||
const { data } = await JobsAPI.readDetail(job.id, type);
|
||||
const { data } = await getJobModel(job.type).readDetail(job.id);
|
||||
setJob(data);
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +1,22 @@
|
||||
export default function isJobRunning(status) {
|
||||
import {
|
||||
JobsAPI,
|
||||
ProjectUpdatesAPI,
|
||||
SystemJobsAPI,
|
||||
WorkflowJobsAPI,
|
||||
InventoryUpdatesAPI,
|
||||
AdHocCommandsAPI,
|
||||
} from '../api';
|
||||
|
||||
export function isJobRunning(status) {
|
||||
return ['new', 'pending', 'waiting', 'running'].includes(status);
|
||||
}
|
||||
|
||||
export function getJobModel(type) {
|
||||
if (type === 'ad_hoc_command') return AdHocCommandsAPI;
|
||||
if (type === 'inventory_update') return InventoryUpdatesAPI;
|
||||
if (type === 'project_update') return ProjectUpdatesAPI;
|
||||
if (type === 'system_job') return SystemJobsAPI;
|
||||
if (type === 'workflow_job') return WorkflowJobsAPI;
|
||||
|
||||
return JobsAPI;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import isJobRunning from './jobs';
|
||||
import { getJobModel, isJobRunning } from './jobs';
|
||||
|
||||
describe('isJobRunning', () => {
|
||||
test('should return true for new', () => {
|
||||
@ -23,3 +23,23 @@ describe('isJobRunning', () => {
|
||||
expect(isJobRunning('failed')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getJobModel', () => {
|
||||
test('should return valid job model in all cases', () => {
|
||||
const baseUrls = [];
|
||||
[
|
||||
'ad_hoc_command',
|
||||
'inventory_update',
|
||||
'project_update',
|
||||
'system_job',
|
||||
'workflow_job',
|
||||
'job',
|
||||
'default',
|
||||
].forEach(type => {
|
||||
expect(getJobModel(type)).toHaveProperty('http');
|
||||
expect(getJobModel(type).jobEventSlug).toBeDefined();
|
||||
baseUrls.push(getJobModel(type).baseUrl);
|
||||
});
|
||||
expect(new Set(baseUrls).size).toBe(baseUrls.length - 1);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user