diff --git a/awx/ui_next/src/api/mixins/Relaunch.mixin.js b/awx/ui_next/src/api/mixins/Relaunch.mixin.js deleted file mode 100644 index 06594c6dd3..0000000000 --- a/awx/ui_next/src/api/mixins/Relaunch.mixin.js +++ /dev/null @@ -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; diff --git a/awx/ui_next/src/api/mixins/Runnable.mixin.js b/awx/ui_next/src/api/mixins/Runnable.mixin.js new file mode 100644 index 0000000000..ba8aac8681 --- /dev/null +++ b/awx/ui_next/src/api/mixins/Runnable.mixin.js @@ -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; diff --git a/awx/ui_next/src/api/models/AdHocCommands.js b/awx/ui_next/src/api/models/AdHocCommands.js index 4879b81b32..2db8e7ddf8 100644 --- a/awx/ui_next/src/api/models/AdHocCommands.js +++ b/awx/ui_next/src/api/models/AdHocCommands.js @@ -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; diff --git a/awx/ui_next/src/api/models/InventoryUpdates.js b/awx/ui_next/src/api/models/InventoryUpdates.js index 1700c7b26b..0d917b0aeb 100644 --- a/awx/ui_next/src/api/models/InventoryUpdates.js +++ b/awx/ui_next/src/api/models/InventoryUpdates.js @@ -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; diff --git a/awx/ui_next/src/api/models/Jobs.js b/awx/ui_next/src/api/models/Jobs.js index 026ae671f0..ae3b94cc31 100644 --- a/awx/ui_next/src/api/models/Jobs.js +++ b/awx/ui_next/src/api/models/Jobs.js @@ -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}/`); } } diff --git a/awx/ui_next/src/api/models/ProjectUpdates.js b/awx/ui_next/src/api/models/ProjectUpdates.js index 46d0633f0d..3925ae95e9 100644 --- a/awx/ui_next/src/api/models/ProjectUpdates.js +++ b/awx/ui_next/src/api/models/ProjectUpdates.js @@ -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; diff --git a/awx/ui_next/src/api/models/SystemJobs.js b/awx/ui_next/src/api/models/SystemJobs.js index d7b6ec1750..8365f6f65b 100644 --- a/awx/ui_next/src/api/models/SystemJobs.js +++ b/awx/ui_next/src/api/models/SystemJobs.js @@ -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; diff --git a/awx/ui_next/src/api/models/WorkflowJobs.js b/awx/ui_next/src/api/models/WorkflowJobs.js index 87e336e8f5..f2799973b0 100644 --- a/awx/ui_next/src/api/models/WorkflowJobs.js +++ b/awx/ui_next/src/api/models/WorkflowJobs.js @@ -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; diff --git a/awx/ui_next/src/components/JobList/JobList.jsx b/awx/ui_next/src/components/JobList/JobList.jsx index 5c331fd8a5..c92096ad68 100644 --- a/awx/ui_next/src/components/JobList/JobList.jsx +++ b/awx/ui_next/src/components/JobList/JobList.jsx @@ -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]), diff --git a/awx/ui_next/src/components/JobList/JobList.test.jsx b/awx/ui_next/src/components/JobList/JobList.test.jsx index 87f74abfeb..45451de8dd 100644 --- a/awx/ui_next/src/components/JobList/JobList.test.jsx +++ b/awx/ui_next/src/components/JobList/JobList.test.jsx @@ -319,13 +319,12 @@ describe('', () => { 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(); }); diff --git a/awx/ui_next/src/components/JobList/JobListCancelButton.jsx b/awx/ui_next/src/components/JobList/JobListCancelButton.jsx index 6f008552b7..efad12993b 100644 --- a/awx/ui_next/src/components/JobList/JobListCancelButton.jsx +++ b/awx/ui_next/src/components/JobList/JobListCancelButton.jsx @@ -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'; diff --git a/awx/ui_next/src/screens/Job/Job.jsx b/awx/ui_next/src/screens/Job/Job.jsx index 8c058bbdf6..a46315c397 100644 --- a/awx/ui_next/src/screens/Job/Job.jsx +++ b/awx/ui_next/src/screens/Job/Job.jsx @@ -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 }) { - - {job && - job.type === 'workflow_job' && [ - - - , - + + {job && [ + + + , + + {job.type === 'workflow_job' ? ( - , - ]} - {job && - job.type !== 'workflow_job' && [ - - - , - + ) : ( - , - - - - {i18n._(t`View Job Details`)} - - - , - ]} + )} + , + + + + {i18n._(t`View Job Details`)} + + + , + ]} diff --git a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx index 678b4c24bc..c4a128eaf9 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,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 }) { )} ))} - {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 && ( { - 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 => { diff --git a/awx/ui_next/src/screens/Job/JobOutput/JobOutput.test.jsx b/awx/ui_next/src/screens/Job/JobOutput/JobOutput.test.jsx index 59efdfe323..72354fe32a 100644 --- a/awx/ui_next/src/screens/Job/JobOutput/JobOutput.test.jsx +++ b/awx/ui_next/src/screens/Job/JobOutput/JobOutput.test.jsx @@ -278,7 +278,7 @@ describe('', () => { 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, diff --git a/awx/ui_next/src/screens/Job/JobTypeRedirect.jsx b/awx/ui_next/src/screens/Job/JobTypeRedirect.jsx index dbf2256fdc..22b70c7f17 100644 --- a/awx/ui_next/src/screens/Job/JobTypeRedirect.jsx +++ b/awx/ui_next/src/screens/Job/JobTypeRedirect.jsx @@ -55,8 +55,8 @@ function JobTypeRedirect({ id, path, view, i18n }) { ); } - const type = JOB_TYPE_URL_SEGMENTS[job.type]; - return ; + const typeSegment = JOB_TYPE_URL_SEGMENTS[job.type]; + return ; } JobTypeRedirect.defaultProps = { diff --git a/awx/ui_next/src/screens/Job/Jobs.jsx b/awx/ui_next/src/screens/Job/Jobs.jsx index 318729407a..f75d560d70 100644 --- a/awx/ui_next/src/screens/Job/Jobs.jsx +++ b/awx/ui_next/src/screens/Job/Jobs.jsx @@ -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 }) { - + diff --git a/awx/ui_next/src/screens/Job/useWsJob.js b/awx/ui_next/src/screens/Job/useWsJob.js index ace2cf2ce6..e9461888d7 100644 --- a/awx/ui_next/src/screens/Job/useWsJob.js +++ b/awx/ui_next/src/screens/Job/useWsJob.js @@ -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); } diff --git a/awx/ui_next/src/util/jobs.js b/awx/ui_next/src/util/jobs.js index e4129388a5..de227ffc59 100644 --- a/awx/ui_next/src/util/jobs.js +++ b/awx/ui_next/src/util/jobs.js @@ -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; +} diff --git a/awx/ui_next/src/util/jobs.test.js b/awx/ui_next/src/util/jobs.test.js index 953b06ba17..6dcab23166 100644 --- a/awx/ui_next/src/util/jobs.test.js +++ b/awx/ui_next/src/util/jobs.test.js @@ -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); + }); +});