Add support for launching job templates from the templates list (#277)

Add support for launching job templates from the templates list
This commit is contained in:
Michael Abashian 2019-06-20 15:21:57 -04:00 committed by GitHub
parent cd672baa13
commit cb50cdce0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 210 additions and 4 deletions

View File

@ -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/`);
}
}

View File

@ -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 (
<Fragment>
<Tooltip
content={i18n._(t`Launch Job`)}
position="top"
>
<div>
<StyledLaunchButton
variant="plain"
aria-label={i18n._(t`Launch`)}
onClick={this.handleLaunch}
>
<RocketIcon />
</StyledLaunchButton>
</div>
</Tooltip>
<AlertModal
isOpen={launchError}
variant="danger"
title={i18n._(t`Error!`)}
onClose={this.handleLaunchErrorClose}
>
{i18n._(t`Failed to launch job.`)}
</AlertModal>
<AlertModal
isOpen={promptError}
variant="info"
title={i18n._(t`Attention!`)}
onClose={this.handlePromptErrorClose}
>
{i18n._(t`Launching jobs with promptable fields is not supported at this time.`)}
</AlertModal>
</Fragment>
);
}
}
export default withI18n()(withRouter(LaunchButton));

View File

@ -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(<LaunchButton templateId={1} />);
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(
<LaunchButton templateId={1} />, {
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(<LaunchButton templateId={1} />);
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();
});
});

View File

@ -0,0 +1 @@
export { default } from './LaunchButton';

View File

@ -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 (
<DataListItem
@ -49,7 +52,14 @@ class TemplateListItem extends Component {
</Link>
</span>
</DataListCell>,
<DataListCell key="type">{toTitleCase(template.type)}</DataListCell>
<DataListCell key="type">{toTitleCase(template.type)}</DataListCell>,
<DataListCell lastcolumn="true" key="launch">
{canLaunch && template.type === 'job_template' && (
<LaunchButton
templateId={template.id}
/>
)}
</DataListCell>
]}
/>
</DataListItemRow>

View File

@ -5,14 +5,36 @@ import { mountWithContexts } from '@testUtils/enzymeHelpers';
import TemplatesListItem from './TemplateListItem';
describe('<TemplatesListItem />', () => {
test('initially render successfully', () => {
mountWithContexts(<TemplatesListItem
test('launch button shown to users with start capabilities', () => {
const wrapper = mountWithContexts(<TemplatesListItem
template={{
id: 1,
name: 'Template 1',
url: '/templates/job_template/1',
type: 'job_template'
type: 'job_template',
summary_fields: {
user_capabilities: {
start: true
}
}
}}
/>);
expect(wrapper.find('LaunchButton').exists()).toBeTruthy();
});
test('launch button hidden from users without start capabilities', () => {
const wrapper = mountWithContexts(<TemplatesListItem
template={{
id: 1,
name: 'Template 1',
url: '/templates/job_template/1',
type: 'job_template',
summary_fields: {
user_capabilities: {
start: false
}
}
}}
/>);
expect(wrapper.find('LaunchButton').exists()).toBeFalsy();
});
});