mirror of
https://github.com/ansible/awx.git
synced 2026-05-08 01:47:35 -02: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:
@@ -4,6 +4,17 @@ class JobTemplates extends Base {
|
|||||||
constructor (http) {
|
constructor (http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/job_templates/';
|
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 styled from 'styled-components';
|
||||||
|
|
||||||
import VerticalSeparator from '@components/VerticalSeparator';
|
import VerticalSeparator from '@components/VerticalSeparator';
|
||||||
|
import LaunchButton from '@components/LaunchButton';
|
||||||
import { toTitleCase } from '@util/strings';
|
import { toTitleCase } from '@util/strings';
|
||||||
|
|
||||||
const DataListCell = styled(PFDataListCell)`
|
const DataListCell = styled(PFDataListCell)`
|
||||||
@@ -17,6 +18,7 @@ const DataListCell = styled(PFDataListCell)`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
justify-content: ${props => (props.lastcolumn ? 'flex-end' : 'inherit')};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -27,6 +29,7 @@ class TemplateListItem extends Component {
|
|||||||
isSelected,
|
isSelected,
|
||||||
onSelect,
|
onSelect,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
const canLaunch = template.summary_fields.user_capabilities.start;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataListItem
|
<DataListItem
|
||||||
@@ -49,7 +52,14 @@ class TemplateListItem extends Component {
|
|||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
</DataListCell>,
|
</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>
|
</DataListItemRow>
|
||||||
|
|||||||
@@ -5,14 +5,36 @@ import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
|||||||
import TemplatesListItem from './TemplateListItem';
|
import TemplatesListItem from './TemplateListItem';
|
||||||
|
|
||||||
describe('<TemplatesListItem />', () => {
|
describe('<TemplatesListItem />', () => {
|
||||||
test('initially render successfully', () => {
|
test('launch button shown to users with start capabilities', () => {
|
||||||
mountWithContexts(<TemplatesListItem
|
const wrapper = mountWithContexts(<TemplatesListItem
|
||||||
template={{
|
template={{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Template 1',
|
name: 'Template 1',
|
||||||
url: '/templates/job_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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user