diff --git a/awx/ui_next/src/api/index.js b/awx/ui_next/src/api/index.js
index 128b9aa706..41bef37154 100644
--- a/awx/ui_next/src/api/index.js
+++ b/awx/ui_next/src/api/index.js
@@ -2,6 +2,7 @@ import AdHocCommands from './models/AdHocCommands';
import Config from './models/Config';
import InstanceGroups from './models/InstanceGroups';
import Inventories from './models/Inventories';
+import InventorySources from './models/InventorySources';
import InventoryUpdates from './models/InventoryUpdates';
import JobTemplates from './models/JobTemplates';
import Jobs from './models/Jobs';
@@ -23,6 +24,7 @@ const AdHocCommandsAPI = new AdHocCommands();
const ConfigAPI = new Config();
const InstanceGroupsAPI = new InstanceGroups();
const InventoriesAPI = new Inventories();
+const InventorySourcesAPI = new InventorySources();
const InventoryUpdatesAPI = new InventoryUpdates();
const JobTemplatesAPI = new JobTemplates();
const JobsAPI = new Jobs();
@@ -45,6 +47,7 @@ export {
ConfigAPI,
InstanceGroupsAPI,
InventoriesAPI,
+ InventorySourcesAPI,
InventoryUpdatesAPI,
JobTemplatesAPI,
JobsAPI,
diff --git a/awx/ui_next/src/api/mixins/LaunchUpdate.mixin.js b/awx/ui_next/src/api/mixins/LaunchUpdate.mixin.js
new file mode 100644
index 0000000000..a4e62afb20
--- /dev/null
+++ b/awx/ui_next/src/api/mixins/LaunchUpdate.mixin.js
@@ -0,0 +1,12 @@
+const LaunchUpdateMixin = parent =>
+ class extends parent {
+ launchUpdate(id, data) {
+ return this.http.post(`${this.baseUrl}${id}/update/`, data);
+ }
+
+ readLaunchUpdate(id) {
+ return this.http.get(`${this.baseUrl}${id}/update/`);
+ }
+ };
+
+export default LaunchUpdateMixin;
diff --git a/awx/ui_next/src/api/mixins/Relaunch.mixin.js b/awx/ui_next/src/api/mixins/Relaunch.mixin.js
new file mode 100644
index 0000000000..06594c6dd3
--- /dev/null
+++ b/awx/ui_next/src/api/mixins/Relaunch.mixin.js
@@ -0,0 +1,12 @@
+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/models/AdHocCommands.js b/awx/ui_next/src/api/models/AdHocCommands.js
index 1bfd78e9cb..4879b81b32 100644
--- a/awx/ui_next/src/api/models/AdHocCommands.js
+++ b/awx/ui_next/src/api/models/AdHocCommands.js
@@ -1,6 +1,7 @@
import Base from '../Base';
+import RelaunchMixin from '../mixins/Relaunch.mixin';
-class AdHocCommands extends Base {
+class AdHocCommands extends RelaunchMixin(Base) {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/ad_hoc_commands/';
diff --git a/awx/ui_next/src/api/models/InventorySources.js b/awx/ui_next/src/api/models/InventorySources.js
new file mode 100644
index 0000000000..be43f988eb
--- /dev/null
+++ b/awx/ui_next/src/api/models/InventorySources.js
@@ -0,0 +1,11 @@
+import Base from '../Base';
+import LaunchUpdateMixin from '../mixins/LaunchUpdate.mixin';
+
+class InventorySources extends LaunchUpdateMixin(Base) {
+ constructor(http) {
+ super(http);
+ this.baseUrl = '/api/v2/inventory_sources/';
+ }
+}
+
+export default InventorySources;
diff --git a/awx/ui_next/src/api/models/InventoryUpdates.js b/awx/ui_next/src/api/models/InventoryUpdates.js
index 0b30042e2c..a4dc05b392 100644
--- a/awx/ui_next/src/api/models/InventoryUpdates.js
+++ b/awx/ui_next/src/api/models/InventoryUpdates.js
@@ -1,6 +1,7 @@
import Base from '../Base';
+import LaunchUpdateMixin from '../mixins/LaunchUpdate.mixin';
-class InventoryUpdates extends Base {
+class InventoryUpdates extends LaunchUpdateMixin(Base) {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/inventory_updates/';
diff --git a/awx/ui_next/src/api/models/Jobs.js b/awx/ui_next/src/api/models/Jobs.js
index 1a01ace0ba..a0f2f874fd 100644
--- a/awx/ui_next/src/api/models/Jobs.js
+++ b/awx/ui_next/src/api/models/Jobs.js
@@ -1,4 +1,5 @@
import Base from '../Base';
+import RelaunchMixin from '../mixins/Relaunch.mixin';
const BASE_URLS = {
playbook: '/jobs/',
@@ -9,7 +10,7 @@ const BASE_URLS = {
workflow: '/workflow_jobs/',
};
-class Jobs extends Base {
+class Jobs extends RelaunchMixin(Base) {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/jobs/';
diff --git a/awx/ui_next/src/api/models/Projects.js b/awx/ui_next/src/api/models/Projects.js
index c9278cf4a0..14a6048b0a 100644
--- a/awx/ui_next/src/api/models/Projects.js
+++ b/awx/ui_next/src/api/models/Projects.js
@@ -1,6 +1,7 @@
import Base from '../Base';
+import LaunchUpdateMixin from '../mixins/LaunchUpdate.mixin';
-class Projects extends Base {
+class Projects extends LaunchUpdateMixin(Base) {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/projects/';
diff --git a/awx/ui_next/src/api/models/WorkflowJobs.js b/awx/ui_next/src/api/models/WorkflowJobs.js
index dc484b1bce..8a7102cc99 100644
--- a/awx/ui_next/src/api/models/WorkflowJobs.js
+++ b/awx/ui_next/src/api/models/WorkflowJobs.js
@@ -1,6 +1,7 @@
import Base from '../Base';
+import RelaunchMixin from '../mixins/Relaunch.mixin';
-class WorkflowJobs extends Base {
+class WorkflowJobs extends RelaunchMixin(Base) {
constructor(http) {
super(http);
this.baseUrl = '/api/v2/workflow_jobs/';
diff --git a/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx b/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx
index 6e7c733319..830a660603 100644
--- a/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx
+++ b/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx
@@ -1,16 +1,25 @@
import React, { Fragment } from 'react';
import { withRouter } from 'react-router-dom';
-import { number } from 'prop-types';
+import { number, shape } from 'prop-types';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import AlertModal from '@components/AlertModal';
import ErrorDetail from '@components/ErrorDetail';
-import { JobTemplatesAPI } from '@api';
+import {
+ AdHocCommandsAPI,
+ InventorySourcesAPI,
+ JobsAPI,
+ JobTemplatesAPI,
+ ProjectsAPI,
+ WorkflowJobsAPI,
+} from '@api';
class LaunchButton extends React.Component {
static propTypes = {
- templateId: number.isRequired,
+ resource: shape({
+ id: number.isRequired,
+ }).isRequired,
};
constructor(props) {
@@ -22,6 +31,7 @@ class LaunchButton extends React.Component {
};
this.handleLaunch = this.handleLaunch.bind(this);
+ this.handleRelaunch = this.handleRelaunch.bind(this);
this.handleLaunchErrorClose = this.handleLaunchErrorClose.bind(this);
this.handlePromptErrorClose = this.handlePromptErrorClose.bind(this);
}
@@ -35,13 +45,56 @@ class LaunchButton extends React.Component {
}
async handleLaunch() {
- const { history, templateId } = this.props;
+ const { history, resource } = this.props;
try {
const { data: launchConfig } = await JobTemplatesAPI.readLaunch(
- templateId
+ resource.id
);
if (launchConfig.can_start_without_user_input) {
- const { data: job } = await JobTemplatesAPI.launch(templateId);
+ const { data: job } = await JobTemplatesAPI.launch(resource.id);
+ history.push(`/jobs/${job.id}/details`);
+ } else {
+ this.setState({ promptError: true });
+ }
+ } catch (err) {
+ this.setState({ launchError: err });
+ }
+ }
+
+ async handleRelaunch() {
+ const { history, resource } = this.props;
+
+ let readRelaunch;
+ let relaunch;
+
+ if (resource.type === 'inventory_update') {
+ // We'll need to handle the scenario where the src no longer exists
+ readRelaunch = InventorySourcesAPI.readLaunchUpdate(
+ resource.inventory_source
+ );
+ relaunch = InventorySourcesAPI.launchUpdate(resource.inventory_source);
+ } else if (resource.type === 'project_update') {
+ // We'll need to handle the scenario where the project no longer exists
+ readRelaunch = ProjectsAPI.readLaunchUpdate(resource.project);
+ relaunch = ProjectsAPI.launchUpdate(resource.project);
+ } else if (resource.type === 'workflow_job') {
+ readRelaunch = WorkflowJobsAPI.readRelaunch(resource.id);
+ relaunch = WorkflowJobsAPI.relaunch(resource.id);
+ } else if (resource.type === 'ad_hoc_command') {
+ readRelaunch = AdHocCommandsAPI.readRelaunch(resource.id);
+ relaunch = AdHocCommandsAPI.relaunch(resource.id);
+ } else if (resource.type === 'job') {
+ readRelaunch = JobsAPI.readRelaunch(resource.id);
+ relaunch = JobsAPI.relaunch(resource.id);
+ }
+
+ try {
+ const { data: relaunchConfig } = await readRelaunch;
+ if (
+ !relaunchConfig.passwords_needed_to_start ||
+ relaunchConfig.passwords_needed_to_start.length === 0
+ ) {
+ const { data: job } = await relaunch;
history.push(`/jobs/${job.id}/details`);
} else {
this.setState({ promptError: true });
@@ -56,26 +109,33 @@ class LaunchButton extends React.Component {
const { i18n, children } = this.props;
return (
- {children(this.handleLaunch)}
-
- {i18n._(t`Failed to launch job.`)}
-
-
-
- {i18n._(
- t`Launching jobs with promptable fields is not supported at this time.`
- )}
-
+ {children({
+ handleLaunch: this.handleLaunch,
+ handleRelaunch: this.handleRelaunch,
+ })}
+ {launchError && (
+
+ {i18n._(t`Failed to launch job.`)}
+
+
+ )}
+ {promptError && (
+
+ {i18n._(
+ t`Launching jobs with promptable fields is not supported at this time.`
+ )}
+
+ )}
);
}
diff --git a/awx/ui_next/src/components/LaunchButton/LaunchButton.test.jsx b/awx/ui_next/src/components/LaunchButton/LaunchButton.test.jsx
index 367a9bf49d..75a6a554f2 100644
--- a/awx/ui_next/src/components/LaunchButton/LaunchButton.test.jsx
+++ b/awx/ui_next/src/components/LaunchButton/LaunchButton.test.jsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
import { sleep } from '@testUtils/testUtils';
import LaunchButton from './LaunchButton';
@@ -13,13 +13,19 @@ describe('LaunchButton', () => {
can_start_without_user_input: true,
},
});
- const children = handleLaunch => (
-
+
+ const children = ({ handleLaunch }) => (
+