mirror of
https://github.com/ansible/awx.git
synced 2026-01-17 04:31:21 -03:30
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:
parent
cd672baa13
commit
cb50cdce0d
@ -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/`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
106
src/components/LaunchButton/LaunchButton.jsx
Normal file
106
src/components/LaunchButton/LaunchButton.jsx
Normal 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));
|
||||
56
src/components/LaunchButton/LaunchButton.test.jsx
Normal file
56
src/components/LaunchButton/LaunchButton.test.jsx
Normal 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();
|
||||
});
|
||||
});
|
||||
1
src/components/LaunchButton/index.js
Normal file
1
src/components/LaunchButton/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './LaunchButton';
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user