mirror of
https://github.com/ansible/awx.git
synced 2026-03-04 18:21: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:
@@ -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 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) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/ad_hoc_commands/';
|
this.baseUrl = '/api/v2/ad_hoc_commands/';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readCredentials(id) {
|
||||||
|
return this.http.get(`${this.baseUrl}${id}/credentials/`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AdHocCommands;
|
export default AdHocCommands;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Base from '../Base';
|
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) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/inventory_updates/';
|
this.baseUrl = '/api/v2/inventory_updates/';
|
||||||
@@ -11,5 +11,9 @@ class InventoryUpdates extends LaunchUpdateMixin(Base) {
|
|||||||
createSyncCancel(sourceId) {
|
createSyncCancel(sourceId) {
|
||||||
return this.http.post(`${this.baseUrl}${sourceId}/cancel/`);
|
return this.http.post(`${this.baseUrl}${sourceId}/cancel/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readCredentials(id) {
|
||||||
|
return this.http.get(`${this.baseUrl}${id}/credentials/`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
export default InventoryUpdates;
|
export default InventoryUpdates;
|
||||||
|
|||||||
@@ -1,67 +1,23 @@
|
|||||||
import Base from '../Base';
|
import Base from '../Base';
|
||||||
import RelaunchMixin from '../mixins/Relaunch.mixin';
|
import RunnableMixin from '../mixins/Runnable.mixin';
|
||||||
|
|
||||||
const getBaseURL = type => {
|
class Jobs extends RunnableMixin(Base) {
|
||||||
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) {
|
|
||||||
constructor(http) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/jobs/';
|
this.baseUrl = '/api/v2/jobs/';
|
||||||
|
this.jobEventSlug = '/job_events/';
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel(id, type) {
|
cancel(id) {
|
||||||
return this.http.post(`/api/v2${getBaseURL(type)}${id}/cancel/`);
|
return this.http.post(`${this.baseUrl}${id}/cancel/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
readCredentials(id, type) {
|
readCredentials(id) {
|
||||||
return this.http.get(`/api/v2${getBaseURL(type)}${id}/credentials/`);
|
return this.http.get(`${this.baseUrl}${id}/credentials/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
readDetail(id, type) {
|
readDetail(id) {
|
||||||
return this.http.get(`/api/v2${getBaseURL(type)}${id}/`);
|
return this.http.get(`${this.baseUrl}${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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import Base from '../Base';
|
import Base from '../Base';
|
||||||
|
import RunnableMixin from '../mixins/Runnable.mixin';
|
||||||
|
|
||||||
class ProjectUpdates extends Base {
|
class ProjectUpdates extends RunnableMixin(Base) {
|
||||||
constructor(http) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/project_updates/';
|
this.baseUrl = '/api/v2/project_updates/';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readCredentials(id) {
|
||||||
|
return this.http.get(`${this.baseUrl}${id}/credentials/`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ProjectUpdates;
|
export default ProjectUpdates;
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import Base from '../Base';
|
import Base from '../Base';
|
||||||
|
|
||||||
class SystemJobs extends Base {
|
import RunnableMixin from '../mixins/Runnable.mixin';
|
||||||
|
|
||||||
|
class SystemJobs extends RunnableMixin(Base) {
|
||||||
constructor(http) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/system_jobs/';
|
this.baseUrl = '/api/v2/system_jobs/';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readCredentials(id) {
|
||||||
|
return this.http.get(`${this.baseUrl}${id}/credentials/`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SystemJobs;
|
export default SystemJobs;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Base from '../Base';
|
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) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/workflow_jobs/';
|
this.baseUrl = '/api/v2/workflow_jobs/';
|
||||||
@@ -10,6 +10,10 @@ class WorkflowJobs extends RelaunchMixin(Base) {
|
|||||||
readNodes(id, params) {
|
readNodes(id, params) {
|
||||||
return this.http.get(`${this.baseUrl}${id}/workflow_nodes/`, { params });
|
return this.http.get(`${this.baseUrl}${id}/workflow_nodes/`, { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readCredentials(id) {
|
||||||
|
return this.http.get(`${this.baseUrl}${id}/credentials/`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WorkflowJobs;
|
export default WorkflowJobs;
|
||||||
|
|||||||
@@ -13,20 +13,12 @@ import useRequest, {
|
|||||||
useDeleteItems,
|
useDeleteItems,
|
||||||
useDismissableError,
|
useDismissableError,
|
||||||
} from '../../util/useRequest';
|
} from '../../util/useRequest';
|
||||||
import isJobRunning from '../../util/jobs';
|
import { isJobRunning, getJobModel } from '../../util/jobs';
|
||||||
import { getQSConfig, parseQueryString } from '../../util/qs';
|
import { getQSConfig, parseQueryString } from '../../util/qs';
|
||||||
import JobListItem from './JobListItem';
|
import JobListItem from './JobListItem';
|
||||||
import JobListCancelButton from './JobListCancelButton';
|
import JobListCancelButton from './JobListCancelButton';
|
||||||
import useWsJobs from './useWsJobs';
|
import useWsJobs from './useWsJobs';
|
||||||
import {
|
import { UnifiedJobsAPI } from '../../api';
|
||||||
AdHocCommandsAPI,
|
|
||||||
InventoryUpdatesAPI,
|
|
||||||
JobsAPI,
|
|
||||||
ProjectUpdatesAPI,
|
|
||||||
SystemJobsAPI,
|
|
||||||
UnifiedJobsAPI,
|
|
||||||
WorkflowJobsAPI,
|
|
||||||
} from '../../api';
|
|
||||||
|
|
||||||
function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
||||||
const qsConfig = getQSConfig(
|
const qsConfig = getQSConfig(
|
||||||
@@ -104,7 +96,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
|||||||
return Promise.all(
|
return Promise.all(
|
||||||
selected.map(job => {
|
selected.map(job => {
|
||||||
if (isJobRunning(job.status)) {
|
if (isJobRunning(job.status)) {
|
||||||
return JobsAPI.cancel(job.id, job.type);
|
return getJobModel(job.type).cancel(job.id);
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
})
|
})
|
||||||
@@ -127,22 +119,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) {
|
|||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
selected.map(({ type, id }) => {
|
selected.map(({ type, id }) => {
|
||||||
switch (type) {
|
return getJobModel(type).destroy(id);
|
||||||
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;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}, [selected]),
|
}, [selected]),
|
||||||
|
|||||||
@@ -319,13 +319,12 @@ describe('<JobList />', () => {
|
|||||||
wrapper.find('JobListCancelButton').invoke('onCancel')();
|
wrapper.find('JobListCancelButton').invoke('onCancel')();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(JobsAPI.cancel).toHaveBeenCalledTimes(6);
|
expect(ProjectUpdatesAPI.cancel).toHaveBeenCalledWith(1);
|
||||||
expect(JobsAPI.cancel).toHaveBeenCalledWith(1, 'project_update');
|
expect(JobsAPI.cancel).toHaveBeenCalledWith(2);
|
||||||
expect(JobsAPI.cancel).toHaveBeenCalledWith(2, 'job');
|
expect(InventoryUpdatesAPI.cancel).toHaveBeenCalledWith(3);
|
||||||
expect(JobsAPI.cancel).toHaveBeenCalledWith(3, 'inventory_update');
|
expect(WorkflowJobsAPI.cancel).toHaveBeenCalledWith(4);
|
||||||
expect(JobsAPI.cancel).toHaveBeenCalledWith(4, 'workflow_job');
|
expect(SystemJobsAPI.cancel).toHaveBeenCalledWith(5);
|
||||||
expect(JobsAPI.cancel).toHaveBeenCalledWith(5, 'system_job');
|
expect(AdHocCommandsAPI.cancel).toHaveBeenCalledWith(6);
|
||||||
expect(JobsAPI.cancel).toHaveBeenCalledWith(6, 'ad_hoc_command');
|
|
||||||
|
|
||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { t } from '@lingui/macro';
|
|||||||
import { arrayOf, func } from 'prop-types';
|
import { arrayOf, func } from 'prop-types';
|
||||||
import { Button, DropdownItem, Tooltip } from '@patternfly/react-core';
|
import { Button, DropdownItem, Tooltip } from '@patternfly/react-core';
|
||||||
import { KebabifiedContext } from '../../contexts/Kebabified';
|
import { KebabifiedContext } from '../../contexts/Kebabified';
|
||||||
import isJobRunning from '../../util/jobs';
|
import { isJobRunning } from '../../util/jobs';
|
||||||
import AlertModal from '../AlertModal';
|
import AlertModal from '../AlertModal';
|
||||||
import { Job } from '../../types';
|
import { Job } from '../../types';
|
||||||
|
|
||||||
|
|||||||
@@ -12,20 +12,32 @@ import { withI18n } from '@lingui/react';
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { CaretLeftIcon } from '@patternfly/react-icons';
|
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||||
import { Card, PageSection } from '@patternfly/react-core';
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
import { JobsAPI } from '../../api';
|
|
||||||
import ContentError from '../../components/ContentError';
|
import ContentError from '../../components/ContentError';
|
||||||
import ContentLoading from '../../components/ContentLoading';
|
import ContentLoading from '../../components/ContentLoading';
|
||||||
import RoutedTabs from '../../components/RoutedTabs';
|
import RoutedTabs from '../../components/RoutedTabs';
|
||||||
import useRequest from '../../util/useRequest';
|
import useRequest from '../../util/useRequest';
|
||||||
|
import { getJobModel } from '../../util/jobs';
|
||||||
import JobDetail from './JobDetail';
|
import JobDetail from './JobDetail';
|
||||||
import JobOutput from './JobOutput';
|
import JobOutput from './JobOutput';
|
||||||
import { WorkflowOutput } from './WorkflowOutput';
|
import { WorkflowOutput } from './WorkflowOutput';
|
||||||
import useWsJob from './useWsJob';
|
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 }) {
|
function Job({ i18n, setBreadcrumb }) {
|
||||||
const { id, type } = useParams();
|
const { id, typeSegment } = useParams();
|
||||||
const match = useRouteMatch();
|
const match = useRouteMatch();
|
||||||
|
|
||||||
|
const type = JOB_URL_SEGMENT_MAP[typeSegment];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
@@ -34,12 +46,11 @@ function Job({ i18n, setBreadcrumb }) {
|
|||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
let eventOptions = {};
|
let eventOptions = {};
|
||||||
const { data: jobDetailData } = await JobsAPI.readDetail(id, type);
|
const { data: jobDetailData } = await getJobModel(type).readDetail(id);
|
||||||
if (jobDetailData.type !== 'workflow_job') {
|
if (type !== 'workflow_job') {
|
||||||
const { data: jobEventOptions } = await JobsAPI.readEventOptions(
|
const { data: jobEventOptions } = await getJobModel(
|
||||||
id,
|
|
||||||
type
|
type
|
||||||
);
|
).readEventOptions(id);
|
||||||
eventOptions = jobEventOptions;
|
eventOptions = jobEventOptions;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -49,7 +60,7 @@ function Job({ i18n, setBreadcrumb }) {
|
|||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
data: { results },
|
data: { results },
|
||||||
} = await JobsAPI.readCredentials(jobDetailData.id, type);
|
} = await getJobModel(type).readCredentials(jobDetailData.id);
|
||||||
|
|
||||||
jobDetailData.summary_fields.credentials = results;
|
jobDetailData.summary_fields.credentials = results;
|
||||||
}
|
}
|
||||||
@@ -125,37 +136,37 @@ function Job({ i18n, setBreadcrumb }) {
|
|||||||
<Card>
|
<Card>
|
||||||
<RoutedTabs tabsArray={tabsArray} />
|
<RoutedTabs tabsArray={tabsArray} />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Redirect from="/jobs/:type/:id" to="/jobs/:type/:id/output" exact />
|
<Redirect
|
||||||
{job &&
|
from="/jobs/:typeSegment/:id"
|
||||||
job.type === 'workflow_job' && [
|
to="/jobs/:typeSegment/:id/output"
|
||||||
<Route key="workflow-details" path="/jobs/workflow/:id/details">
|
exact
|
||||||
<JobDetail type={match.params.type} job={job} />
|
/>
|
||||||
</Route>,
|
{job && [
|
||||||
<Route key="workflow-output" path="/jobs/workflow/:id/output">
|
<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} />
|
<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
|
<JobOutput
|
||||||
type={type}
|
|
||||||
job={job}
|
job={job}
|
||||||
eventRelatedSearchableKeys={eventRelatedSearchableKeys}
|
eventRelatedSearchableKeys={eventRelatedSearchableKeys}
|
||||||
eventSearchableKeys={eventSearchableKeys}
|
eventSearchableKeys={eventSearchableKeys}
|
||||||
/>
|
/>
|
||||||
</Route>,
|
)}
|
||||||
<Route key="not-found" path="*">
|
</Route>,
|
||||||
<ContentError isNotFound>
|
<Route key="not-found" path="*">
|
||||||
<Link to={`/jobs/${type}/${id}/details`}>
|
<ContentError isNotFound>
|
||||||
{i18n._(t`View Job Details`)}
|
<Link to={`/jobs/${typeSegment}/${id}/details`}>
|
||||||
</Link>
|
{i18n._(t`View Job Details`)}
|
||||||
</ContentError>
|
</Link>
|
||||||
</Route>,
|
</ContentError>
|
||||||
]}
|
</Route>,
|
||||||
|
]}
|
||||||
</Switch>
|
</Switch>
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'styled-components/macro';
|
import 'styled-components/macro';
|
||||||
import React, { useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
@@ -25,17 +25,11 @@ import {
|
|||||||
} from '../../../components/LaunchButton';
|
} from '../../../components/LaunchButton';
|
||||||
import StatusIcon from '../../../components/StatusIcon';
|
import StatusIcon from '../../../components/StatusIcon';
|
||||||
import ExecutionEnvironmentDetail from '../../../components/ExecutionEnvironmentDetail';
|
import ExecutionEnvironmentDetail from '../../../components/ExecutionEnvironmentDetail';
|
||||||
|
import { getJobModel, isJobRunning } from '../../../util/jobs';
|
||||||
import { toTitleCase } from '../../../util/strings';
|
import { toTitleCase } from '../../../util/strings';
|
||||||
|
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||||
import { formatDateString } from '../../../util/dates';
|
import { formatDateString } from '../../../util/dates';
|
||||||
import { Job } from '../../../types';
|
import { Job } from '../../../types';
|
||||||
import {
|
|
||||||
JobsAPI,
|
|
||||||
ProjectUpdatesAPI,
|
|
||||||
SystemJobsAPI,
|
|
||||||
WorkflowJobsAPI,
|
|
||||||
InventoriesAPI,
|
|
||||||
AdHocCommandsAPI,
|
|
||||||
} from '../../../api';
|
|
||||||
|
|
||||||
const VariablesInput = styled(_VariablesInput)`
|
const VariablesInput = styled(_VariablesInput)`
|
||||||
.pf-c-form__label {
|
.pf-c-form__label {
|
||||||
@@ -77,6 +71,24 @@ function JobDetail({ job, i18n }) {
|
|||||||
const [errorMsg, setErrorMsg] = useState();
|
const [errorMsg, setErrorMsg] = useState();
|
||||||
const history = useHistory();
|
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 = {
|
const jobTypes = {
|
||||||
project_update: i18n._(t`Source Control Update`),
|
project_update: i18n._(t`Source Control Update`),
|
||||||
inventory_update: i18n._(t`Inventory Sync`),
|
inventory_update: i18n._(t`Inventory Sync`),
|
||||||
@@ -91,25 +103,7 @@ function JobDetail({ job, i18n }) {
|
|||||||
|
|
||||||
const deleteJob = async () => {
|
const deleteJob = async () => {
|
||||||
try {
|
try {
|
||||||
switch (job.type) {
|
await getJobModel(job.type).destroy(job.id);
|
||||||
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');
|
history.push('/jobs');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setErrorMsg(err);
|
setErrorMsg(err);
|
||||||
@@ -410,16 +404,75 @@ function JobDetail({ job, i18n }) {
|
|||||||
)}
|
)}
|
||||||
</LaunchButton>
|
</LaunchButton>
|
||||||
))}
|
))}
|
||||||
{job.summary_fields.user_capabilities.delete && (
|
{isJobRunning(job.status) &&
|
||||||
<DeleteButton
|
job?.summary_fields?.user_capabilities?.start && (
|
||||||
name={job.name}
|
<Button
|
||||||
modalTitle={i18n._(t`Delete Job`)}
|
variant="secondary"
|
||||||
onConfirm={deleteJob}
|
aria-label={i18n._(t`Cancel`)}
|
||||||
>
|
isDisabled={isCancelling}
|
||||||
{i18n._(t`Delete`)}
|
onClick={() => setShowCancelModal(true)}
|
||||||
</DeleteButton>
|
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>
|
</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 && (
|
{errorMsg && (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={errorMsg}
|
isOpen={errorMsg}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import PageControls from './PageControls';
|
|||||||
import HostEventModal from './HostEventModal';
|
import HostEventModal from './HostEventModal';
|
||||||
import { HostStatusBar, OutputToolbar } from './shared';
|
import { HostStatusBar, OutputToolbar } from './shared';
|
||||||
import getRowRangePageSize from './shared/jobOutputUtils';
|
import getRowRangePageSize from './shared/jobOutputUtils';
|
||||||
import isJobRunning from '../../../util/jobs';
|
import { getJobModel, isJobRunning } from '../../../util/jobs';
|
||||||
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||||
import {
|
import {
|
||||||
encodeNonDefaultQueryString,
|
encodeNonDefaultQueryString,
|
||||||
@@ -47,14 +47,6 @@ import {
|
|||||||
removeParams,
|
removeParams,
|
||||||
getQSConfig,
|
getQSConfig,
|
||||||
} from '../../../util/qs';
|
} from '../../../util/qs';
|
||||||
import {
|
|
||||||
JobsAPI,
|
|
||||||
ProjectUpdatesAPI,
|
|
||||||
SystemJobsAPI,
|
|
||||||
WorkflowJobsAPI,
|
|
||||||
InventoriesAPI,
|
|
||||||
AdHocCommandsAPI,
|
|
||||||
} from '../../../api';
|
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('job_output', {
|
const QS_CONFIG = getQSConfig('job_output', {
|
||||||
order_by: 'start_line',
|
order_by: 'start_line',
|
||||||
@@ -280,12 +272,7 @@ const cache = new CellMeasurerCache({
|
|||||||
defaultHeight: 25,
|
defaultHeight: 25,
|
||||||
});
|
});
|
||||||
|
|
||||||
function JobOutput({
|
function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
||||||
job,
|
|
||||||
type,
|
|
||||||
eventRelatedSearchableKeys,
|
|
||||||
eventSearchableKeys,
|
|
||||||
}) {
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const listRef = useRef(null);
|
const listRef = useRef(null);
|
||||||
const isMounted = useRef(false);
|
const isMounted = useRef(false);
|
||||||
@@ -348,8 +335,8 @@ function JobOutput({
|
|||||||
request: cancelJob,
|
request: cancelJob,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
await JobsAPI.cancel(job.id, type);
|
await getJobModel(job.type).cancel(job.id);
|
||||||
}, [job.id, type]),
|
}, [job.id, job.type]),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -364,27 +351,10 @@ function JobOutput({
|
|||||||
error: deleteError,
|
error: deleteError,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
switch (job.type) {
|
await getJobModel(job.type).destroy(job.id);
|
||||||
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');
|
history.push('/jobs');
|
||||||
}, [job, history])
|
}, [job.type, job.id, history])
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -417,7 +387,7 @@ function JobOutput({
|
|||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
data: { results: fetchedEvents = [], count },
|
data: { results: fetchedEvents = [], count },
|
||||||
} = await JobsAPI.readEvents(job.id, type, {
|
} = await getJobModel(job.type).readEvents(job.id, {
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 50,
|
page_size: 50,
|
||||||
...parseQueryString(QS_CONFIG, location.search),
|
...parseQueryString(QS_CONFIG, location.search),
|
||||||
@@ -557,31 +527,33 @@ function JobOutput({
|
|||||||
...parseQueryString(QS_CONFIG, location.search),
|
...parseQueryString(QS_CONFIG, location.search),
|
||||||
};
|
};
|
||||||
|
|
||||||
return JobsAPI.readEvents(job.id, type, params).then(response => {
|
return getJobModel(job.type)
|
||||||
if (isMounted.current) {
|
.readEvents(job.id, params)
|
||||||
const newResults = {};
|
.then(response => {
|
||||||
let newResultsCssMap = {};
|
if (isMounted.current) {
|
||||||
response.data.results.forEach((jobEvent, index) => {
|
const newResults = {};
|
||||||
newResults[firstIndex + index] = jobEvent;
|
let newResultsCssMap = {};
|
||||||
const { lineCssMap } = getLineTextHtml(jobEvent);
|
response.data.results.forEach((jobEvent, index) => {
|
||||||
newResultsCssMap = { ...newResultsCssMap, ...lineCssMap };
|
newResults[firstIndex + index] = jobEvent;
|
||||||
});
|
const { lineCssMap } = getLineTextHtml(jobEvent);
|
||||||
setResults(prevResults => ({
|
newResultsCssMap = { ...newResultsCssMap, ...lineCssMap };
|
||||||
...prevResults,
|
});
|
||||||
...newResults,
|
setResults(prevResults => ({
|
||||||
}));
|
...prevResults,
|
||||||
setCssMap(prevCssMap => ({
|
...newResults,
|
||||||
...prevCssMap,
|
}));
|
||||||
...newResultsCssMap,
|
setCssMap(prevCssMap => ({
|
||||||
}));
|
...prevCssMap,
|
||||||
setCurrentlyLoading(prevCurrentlyLoading =>
|
...newResultsCssMap,
|
||||||
prevCurrentlyLoading.filter(n => !loadRange.includes(n))
|
}));
|
||||||
);
|
setCurrentlyLoading(prevCurrentlyLoading =>
|
||||||
loadRange.forEach(n => {
|
prevCurrentlyLoading.filter(n => !loadRange.includes(n))
|
||||||
cache.clear(n);
|
);
|
||||||
});
|
loadRange.forEach(n => {
|
||||||
}
|
cache.clear(n);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrollToRow = rowIndex => {
|
const scrollToRow = rowIndex => {
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ describe('<JobOutput />', () => {
|
|||||||
wrapper.find(searchBtn).simulate('click');
|
wrapper.find(searchBtn).simulate('click');
|
||||||
});
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
expect(JobsAPI.readEvents).toHaveBeenCalledWith(2, undefined, {
|
expect(JobsAPI.readEvents).toHaveBeenCalledWith(2, {
|
||||||
order_by: 'start_line',
|
order_by: 'start_line',
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 50,
|
page_size: 50,
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ function JobTypeRedirect({ id, path, view, i18n }) {
|
|||||||
</PageSection>
|
</PageSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const type = JOB_TYPE_URL_SEGMENTS[job.type];
|
const typeSegment = JOB_TYPE_URL_SEGMENTS[job.type];
|
||||||
return <Redirect from={path} to={`/jobs/${type}/${job.id}/${view}`} />;
|
return <Redirect from={path} to={`/jobs/${typeSegment}/${job.id}/${view}`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
JobTypeRedirect.defaultProps = {
|
JobTypeRedirect.defaultProps = {
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ function Jobs({ i18n }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const type = JOB_TYPE_URL_SEGMENTS[job.type];
|
const typeSegment = JOB_TYPE_URL_SEGMENTS[job.type];
|
||||||
setBreadcrumbConfig({
|
setBreadcrumbConfig({
|
||||||
'/jobs': i18n._(t`Jobs`),
|
'/jobs': i18n._(t`Jobs`),
|
||||||
[`/jobs/${type}/${job.id}`]: `${job.name}`,
|
[`/jobs/${typeSegment}/${job.id}`]: `${job.name}`,
|
||||||
[`/jobs/${type}/${job.id}/output`]: i18n._(t`Output`),
|
[`/jobs/${typeSegment}/${job.id}/output`]: i18n._(t`Output`),
|
||||||
[`/jobs/${type}/${job.id}/details`]: i18n._(t`Details`),
|
[`/jobs/${typeSegment}/${job.id}/details`]: i18n._(t`Details`),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[i18n]
|
[i18n]
|
||||||
@@ -53,7 +53,7 @@ function Jobs({ i18n }) {
|
|||||||
<Route path={`${match.path}/:id/output`}>
|
<Route path={`${match.path}/:id/output`}>
|
||||||
<TypeRedirect view="output" />
|
<TypeRedirect view="output" />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${match.path}/:type/:id`}>
|
<Route path={`${match.path}/:typeSegment/:id`}>
|
||||||
<Job setBreadcrumb={buildBreadcrumbConfig} />
|
<Job setBreadcrumb={buildBreadcrumbConfig} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${match.path}/:id`}>
|
<Route path={`${match.path}/:id`}>
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import useWebsocket from '../../util/useWebsocket';
|
import useWebsocket from '../../util/useWebsocket';
|
||||||
import { JobsAPI } from '../../api';
|
import { getJobModel } from '../../util/jobs';
|
||||||
|
|
||||||
export default function useWsJob(initialJob) {
|
export default function useWsJob(initialJob) {
|
||||||
const { type } = useParams();
|
|
||||||
const [job, setJob] = useState(initialJob);
|
const [job, setJob] = useState(initialJob);
|
||||||
const lastMessage = useWebsocket({
|
const lastMessage = useWebsocket({
|
||||||
jobs: ['status_changed'],
|
jobs: ['status_changed'],
|
||||||
@@ -18,7 +16,7 @@ export default function useWsJob(initialJob) {
|
|||||||
useEffect(
|
useEffect(
|
||||||
function parseWsMessage() {
|
function parseWsMessage() {
|
||||||
async function fetchJob() {
|
async function fetchJob() {
|
||||||
const { data } = await JobsAPI.readDetail(job.id, type);
|
const { data } = await getJobModel(job.type).readDetail(job.id);
|
||||||
setJob(data);
|
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);
|
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', () => {
|
describe('isJobRunning', () => {
|
||||||
test('should return true for new', () => {
|
test('should return true for new', () => {
|
||||||
@@ -23,3 +23,23 @@ describe('isJobRunning', () => {
|
|||||||
expect(isJobRunning('failed')).toBe(false);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user