From cb50cdce0dd8d2c79ab36de5b634c9f433580831 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Thu, 20 Jun 2019 15:21:57 -0400 Subject: [PATCH] Add support for launching job templates from the templates list (#277) Add support for launching job templates from the templates list --- src/api/models/JobTemplates.js | 11 ++ src/components/LaunchButton/LaunchButton.jsx | 106 ++++++++++++++++++ .../LaunchButton/LaunchButton.test.jsx | 56 +++++++++ src/components/LaunchButton/index.js | 1 + .../TemplateList/TemplateListItem.jsx | 12 +- .../TemplateList/TemplatesListItem.test.jsx | 28 ++++- 6 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 src/components/LaunchButton/LaunchButton.jsx create mode 100644 src/components/LaunchButton/LaunchButton.test.jsx create mode 100644 src/components/LaunchButton/index.js 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(); }); });