diff --git a/src/api/models/JobTemplates.js b/src/api/models/JobTemplates.js
index 3ce27b70ff..3158d48c41 100644
--- a/src/api/models/JobTemplates.js
+++ b/src/api/models/JobTemplates.js
@@ -4,6 +4,17 @@ class JobTemplates extends Base {
constructor (http) {
super(http);
this.baseUrl = '/api/v2/job_templates/';
+
+ this.launch = this.launch.bind(this);
+ this.readLaunch = this.readLaunch.bind(this);
+ }
+
+ launch (id, data) {
+ return this.http.post(`${this.baseUrl}${id}/launch/`, data);
+ }
+
+ readLaunch (id) {
+ return this.http.get(`${this.baseUrl}${id}/launch/`);
}
}
diff --git a/src/components/LaunchButton/LaunchButton.jsx b/src/components/LaunchButton/LaunchButton.jsx
new file mode 100644
index 0000000000..33153285e9
--- /dev/null
+++ b/src/components/LaunchButton/LaunchButton.jsx
@@ -0,0 +1,106 @@
+import React, { Fragment } from 'react';
+import { withRouter } from 'react-router-dom';
+import { number } from 'prop-types';
+import { Button, Tooltip } from '@patternfly/react-core';
+import { RocketIcon } from '@patternfly/react-icons';
+import styled from 'styled-components';
+import { withI18n } from '@lingui/react';
+import { t } from '@lingui/macro';
+
+import AlertModal from '@components/AlertModal';
+import { JobTemplatesAPI } from '@api';
+
+const StyledLaunchButton = styled(Button)`
+ padding: 5px 8px;
+
+ &:hover {
+ background-color:#d9534f;
+ color: white;
+ }
+`;
+
+class LaunchButton extends React.Component {
+ static propTypes = {
+ templateId: number.isRequired,
+ };
+
+ constructor (props) {
+ super(props);
+
+ this.state = {
+ launchError: false,
+ promptError: false
+ };
+
+ this.handleLaunch = this.handleLaunch.bind(this);
+ this.handleLaunchErrorClose = this.handleLaunchErrorClose.bind(this);
+ this.handlePromptErrorClose = this.handlePromptErrorClose.bind(this);
+ }
+
+ handleLaunchErrorClose () {
+ this.setState({ launchError: false });
+ }
+
+ handlePromptErrorClose () {
+ this.setState({ promptError: false });
+ }
+
+ async handleLaunch () {
+ const { history, templateId } = this.props;
+ try {
+ const { data: launchConfig } = await JobTemplatesAPI.readLaunch(templateId);
+ if (launchConfig.can_start_without_user_input) {
+ const { data: job } = await JobTemplatesAPI.launch(templateId);
+ history.push(`/jobs/${job.id}/details`);
+ } else {
+ this.setState({ promptError: true });
+ }
+ } catch (error) {
+ this.setState({ launchError: true });
+ }
+ }
+
+ render () {
+ const {
+ launchError,
+ promptError
+ } = this.state;
+ const { i18n } = this.props;
+ return (
+
+
+
+
+
+
+
+
+
+ {i18n._(t`Failed to launch job.`)}
+
+
+ {i18n._(t`Launching jobs with promptable fields is not supported at this time.`)}
+
+
+ );
+ }
+}
+
+export default withI18n()(withRouter(LaunchButton));
diff --git a/src/components/LaunchButton/LaunchButton.test.jsx b/src/components/LaunchButton/LaunchButton.test.jsx
new file mode 100644
index 0000000000..1de694bd4b
--- /dev/null
+++ b/src/components/LaunchButton/LaunchButton.test.jsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
+import { sleep } from '@testUtils/testUtils';
+
+import LaunchButton from './LaunchButton';
+import { JobTemplatesAPI } from '@api';
+
+jest.mock('@api');
+
+describe('LaunchButton', () => {
+ JobTemplatesAPI.readLaunch.mockResolvedValue({
+ data: {
+ can_start_without_user_input: true
+ }
+ });
+
+ test('renders the expected content', () => {
+ const wrapper = mountWithContexts();
+ expect(wrapper).toHaveLength(1);
+ });
+ test('redirects to details after successful launch', async (done) => {
+ const history = {
+ push: jest.fn(),
+ };
+ JobTemplatesAPI.launch.mockResolvedValue({
+ data: {
+ id: 9000
+ }
+ });
+ const wrapper = mountWithContexts(
+ , {
+ context: {
+ router: { history }
+ }
+ }
+ );
+ const launchButton = wrapper.find('LaunchButton__StyledLaunchButton');
+ launchButton.simulate('click');
+ await sleep(0);
+ expect(JobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1);
+ expect(JobTemplatesAPI.launch).toHaveBeenCalledWith(1);
+ expect(history.push).toHaveBeenCalledWith('/jobs/9000/details');
+ done();
+ });
+ test('displays error modal after unsuccessful launch', async (done) => {
+ JobTemplatesAPI.launch.mockRejectedValue({});
+ const wrapper = mountWithContexts();
+ const launchButton = wrapper.find('LaunchButton__StyledLaunchButton');
+ launchButton.simulate('click');
+ await waitForElement(wrapper, 'Modal.at-c-alertModal--danger', (el) => el.props().isOpen === true && el.props().title === 'Error!');
+ const modalCloseButton = wrapper.find('ModalBoxCloseButton');
+ modalCloseButton.simulate('click');
+ await waitForElement(wrapper, 'Modal.at-c-alertModal--danger', (el) => el.props().isOpen === false);
+ done();
+ });
+});
diff --git a/src/components/LaunchButton/index.js b/src/components/LaunchButton/index.js
new file mode 100644
index 0000000000..ed31194c06
--- /dev/null
+++ b/src/components/LaunchButton/index.js
@@ -0,0 +1 @@
+export { default } from './LaunchButton';
diff --git a/src/screens/Template/TemplateList/TemplateListItem.jsx b/src/screens/Template/TemplateList/TemplateListItem.jsx
index 0423bd1396..0937a0e584 100644
--- a/src/screens/Template/TemplateList/TemplateListItem.jsx
+++ b/src/screens/Template/TemplateList/TemplateListItem.jsx
@@ -10,6 +10,7 @@ import {
import styled from 'styled-components';
import VerticalSeparator from '@components/VerticalSeparator';
+import LaunchButton from '@components/LaunchButton';
import { toTitleCase } from '@util/strings';
const DataListCell = styled(PFDataListCell)`
@@ -17,6 +18,7 @@ const DataListCell = styled(PFDataListCell)`
align-items: center;
@media screen and (min-width: 768px) {
padding-bottom: 0;
+ justify-content: ${props => (props.lastcolumn ? 'flex-end' : 'inherit')};
}
`;
@@ -27,6 +29,7 @@ class TemplateListItem extends Component {
isSelected,
onSelect,
} = this.props;
+ const canLaunch = template.summary_fields.user_capabilities.start;
return (
,
- {toTitleCase(template.type)}
+ {toTitleCase(template.type)},
+
+ {canLaunch && template.type === 'job_template' && (
+
+ )}
+
]}
/>
diff --git a/src/screens/Template/TemplateList/TemplatesListItem.test.jsx b/src/screens/Template/TemplateList/TemplatesListItem.test.jsx
index 6bf7f5e8df..911d820508 100644
--- a/src/screens/Template/TemplateList/TemplatesListItem.test.jsx
+++ b/src/screens/Template/TemplateList/TemplatesListItem.test.jsx
@@ -5,14 +5,36 @@ import { mountWithContexts } from '@testUtils/enzymeHelpers';
import TemplatesListItem from './TemplateListItem';
describe('', () => {
- test('initially render successfully', () => {
- mountWithContexts( {
+ const wrapper = mountWithContexts();
+ expect(wrapper.find('LaunchButton').exists()).toBeTruthy();
+ });
+ test('launch button hidden from users without start capabilities', () => {
+ const wrapper = mountWithContexts();
+ expect(wrapper.find('LaunchButton').exists()).toBeFalsy();
});
});