mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 02:19:58 -03:30
Merge pull request #4792 from mabashian/relaunch-jobs
Add relaunch to Jobs list and Job Details views Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
commit
d2a5af44de
@ -2,6 +2,7 @@ import AdHocCommands from './models/AdHocCommands';
|
||||
import Config from './models/Config';
|
||||
import InstanceGroups from './models/InstanceGroups';
|
||||
import Inventories from './models/Inventories';
|
||||
import InventorySources from './models/InventorySources';
|
||||
import InventoryUpdates from './models/InventoryUpdates';
|
||||
import JobTemplates from './models/JobTemplates';
|
||||
import Jobs from './models/Jobs';
|
||||
@ -23,6 +24,7 @@ const AdHocCommandsAPI = new AdHocCommands();
|
||||
const ConfigAPI = new Config();
|
||||
const InstanceGroupsAPI = new InstanceGroups();
|
||||
const InventoriesAPI = new Inventories();
|
||||
const InventorySourcesAPI = new InventorySources();
|
||||
const InventoryUpdatesAPI = new InventoryUpdates();
|
||||
const JobTemplatesAPI = new JobTemplates();
|
||||
const JobsAPI = new Jobs();
|
||||
@ -45,6 +47,7 @@ export {
|
||||
ConfigAPI,
|
||||
InstanceGroupsAPI,
|
||||
InventoriesAPI,
|
||||
InventorySourcesAPI,
|
||||
InventoryUpdatesAPI,
|
||||
JobTemplatesAPI,
|
||||
JobsAPI,
|
||||
|
||||
12
awx/ui_next/src/api/mixins/LaunchUpdate.mixin.js
Normal file
12
awx/ui_next/src/api/mixins/LaunchUpdate.mixin.js
Normal file
@ -0,0 +1,12 @@
|
||||
const LaunchUpdateMixin = parent =>
|
||||
class extends parent {
|
||||
launchUpdate(id, data) {
|
||||
return this.http.post(`${this.baseUrl}${id}/update/`, data);
|
||||
}
|
||||
|
||||
readLaunchUpdate(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/update/`);
|
||||
}
|
||||
};
|
||||
|
||||
export default LaunchUpdateMixin;
|
||||
12
awx/ui_next/src/api/mixins/Relaunch.mixin.js
Normal file
12
awx/ui_next/src/api/mixins/Relaunch.mixin.js
Normal file
@ -0,0 +1,12 @@
|
||||
const RelaunchMixin = parent =>
|
||||
class extends parent {
|
||||
relaunch(id, data) {
|
||||
return this.http.post(`${this.baseUrl}${id}/relaunch/`, data);
|
||||
}
|
||||
|
||||
readRelaunch(id) {
|
||||
return this.http.get(`${this.baseUrl}${id}/relaunch/`);
|
||||
}
|
||||
};
|
||||
|
||||
export default RelaunchMixin;
|
||||
@ -1,6 +1,7 @@
|
||||
import Base from '../Base';
|
||||
import RelaunchMixin from '../mixins/Relaunch.mixin';
|
||||
|
||||
class AdHocCommands extends Base {
|
||||
class AdHocCommands extends RelaunchMixin(Base) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/ad_hoc_commands/';
|
||||
|
||||
11
awx/ui_next/src/api/models/InventorySources.js
Normal file
11
awx/ui_next/src/api/models/InventorySources.js
Normal file
@ -0,0 +1,11 @@
|
||||
import Base from '../Base';
|
||||
import LaunchUpdateMixin from '../mixins/LaunchUpdate.mixin';
|
||||
|
||||
class InventorySources extends LaunchUpdateMixin(Base) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/inventory_sources/';
|
||||
}
|
||||
}
|
||||
|
||||
export default InventorySources;
|
||||
@ -1,6 +1,7 @@
|
||||
import Base from '../Base';
|
||||
import LaunchUpdateMixin from '../mixins/LaunchUpdate.mixin';
|
||||
|
||||
class InventoryUpdates extends Base {
|
||||
class InventoryUpdates extends LaunchUpdateMixin(Base) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/inventory_updates/';
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import Base from '../Base';
|
||||
import RelaunchMixin from '../mixins/Relaunch.mixin';
|
||||
|
||||
const BASE_URLS = {
|
||||
playbook: '/jobs/',
|
||||
@ -9,7 +10,7 @@ const BASE_URLS = {
|
||||
workflow: '/workflow_jobs/',
|
||||
};
|
||||
|
||||
class Jobs extends Base {
|
||||
class Jobs extends RelaunchMixin(Base) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/jobs/';
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import Base from '../Base';
|
||||
import LaunchUpdateMixin from '../mixins/LaunchUpdate.mixin';
|
||||
|
||||
class Projects extends Base {
|
||||
class Projects extends LaunchUpdateMixin(Base) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/projects/';
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import Base from '../Base';
|
||||
import RelaunchMixin from '../mixins/Relaunch.mixin';
|
||||
|
||||
class WorkflowJobs extends Base {
|
||||
class WorkflowJobs extends RelaunchMixin(Base) {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/workflow_jobs/';
|
||||
|
||||
@ -1,16 +1,25 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { number } from 'prop-types';
|
||||
import { number, shape } from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import AlertModal from '@components/AlertModal';
|
||||
import ErrorDetail from '@components/ErrorDetail';
|
||||
import { JobTemplatesAPI } from '@api';
|
||||
import {
|
||||
AdHocCommandsAPI,
|
||||
InventorySourcesAPI,
|
||||
JobsAPI,
|
||||
JobTemplatesAPI,
|
||||
ProjectsAPI,
|
||||
WorkflowJobsAPI,
|
||||
} from '@api';
|
||||
|
||||
class LaunchButton extends React.Component {
|
||||
static propTypes = {
|
||||
templateId: number.isRequired,
|
||||
resource: shape({
|
||||
id: number.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@ -22,6 +31,7 @@ class LaunchButton extends React.Component {
|
||||
};
|
||||
|
||||
this.handleLaunch = this.handleLaunch.bind(this);
|
||||
this.handleRelaunch = this.handleRelaunch.bind(this);
|
||||
this.handleLaunchErrorClose = this.handleLaunchErrorClose.bind(this);
|
||||
this.handlePromptErrorClose = this.handlePromptErrorClose.bind(this);
|
||||
}
|
||||
@ -35,13 +45,56 @@ class LaunchButton extends React.Component {
|
||||
}
|
||||
|
||||
async handleLaunch() {
|
||||
const { history, templateId } = this.props;
|
||||
const { history, resource } = this.props;
|
||||
try {
|
||||
const { data: launchConfig } = await JobTemplatesAPI.readLaunch(
|
||||
templateId
|
||||
resource.id
|
||||
);
|
||||
if (launchConfig.can_start_without_user_input) {
|
||||
const { data: job } = await JobTemplatesAPI.launch(templateId);
|
||||
const { data: job } = await JobTemplatesAPI.launch(resource.id);
|
||||
history.push(`/jobs/${job.id}/details`);
|
||||
} else {
|
||||
this.setState({ promptError: true });
|
||||
}
|
||||
} catch (err) {
|
||||
this.setState({ launchError: err });
|
||||
}
|
||||
}
|
||||
|
||||
async handleRelaunch() {
|
||||
const { history, resource } = this.props;
|
||||
|
||||
let readRelaunch;
|
||||
let relaunch;
|
||||
|
||||
if (resource.type === 'inventory_update') {
|
||||
// We'll need to handle the scenario where the src no longer exists
|
||||
readRelaunch = InventorySourcesAPI.readLaunchUpdate(
|
||||
resource.inventory_source
|
||||
);
|
||||
relaunch = InventorySourcesAPI.launchUpdate(resource.inventory_source);
|
||||
} else if (resource.type === 'project_update') {
|
||||
// We'll need to handle the scenario where the project no longer exists
|
||||
readRelaunch = ProjectsAPI.readLaunchUpdate(resource.project);
|
||||
relaunch = ProjectsAPI.launchUpdate(resource.project);
|
||||
} else if (resource.type === 'workflow_job') {
|
||||
readRelaunch = WorkflowJobsAPI.readRelaunch(resource.id);
|
||||
relaunch = WorkflowJobsAPI.relaunch(resource.id);
|
||||
} else if (resource.type === 'ad_hoc_command') {
|
||||
readRelaunch = AdHocCommandsAPI.readRelaunch(resource.id);
|
||||
relaunch = AdHocCommandsAPI.relaunch(resource.id);
|
||||
} else if (resource.type === 'job') {
|
||||
readRelaunch = JobsAPI.readRelaunch(resource.id);
|
||||
relaunch = JobsAPI.relaunch(resource.id);
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: relaunchConfig } = await readRelaunch;
|
||||
if (
|
||||
!relaunchConfig.passwords_needed_to_start ||
|
||||
relaunchConfig.passwords_needed_to_start.length === 0
|
||||
) {
|
||||
const { data: job } = await relaunch;
|
||||
history.push(`/jobs/${job.id}/details`);
|
||||
} else {
|
||||
this.setState({ promptError: true });
|
||||
@ -56,26 +109,33 @@ class LaunchButton extends React.Component {
|
||||
const { i18n, children } = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
{children(this.handleLaunch)}
|
||||
<AlertModal
|
||||
isOpen={launchError}
|
||||
variant="danger"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={this.handleLaunchErrorClose}
|
||||
>
|
||||
{i18n._(t`Failed to launch job.`)}
|
||||
<ErrorDetail error={launchError} />
|
||||
</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>
|
||||
{children({
|
||||
handleLaunch: this.handleLaunch,
|
||||
handleRelaunch: this.handleRelaunch,
|
||||
})}
|
||||
{launchError && (
|
||||
<AlertModal
|
||||
isOpen={launchError}
|
||||
variant="danger"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={this.handleLaunchErrorClose}
|
||||
>
|
||||
{i18n._(t`Failed to launch job.`)}
|
||||
<ErrorDetail error={launchError} />
|
||||
</AlertModal>
|
||||
)}
|
||||
{promptError && (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import { sleep } from '@testUtils/testUtils';
|
||||
|
||||
import LaunchButton from './LaunchButton';
|
||||
@ -13,13 +13,19 @@ describe('LaunchButton', () => {
|
||||
can_start_without_user_input: true,
|
||||
},
|
||||
});
|
||||
const children = handleLaunch => (
|
||||
<button type="submit" onClick={handleLaunch} />
|
||||
|
||||
const children = ({ handleLaunch }) => (
|
||||
<button type="submit" onClick={() => handleLaunch()} />
|
||||
);
|
||||
|
||||
const resource = {
|
||||
id: 1,
|
||||
type: 'job_template',
|
||||
};
|
||||
|
||||
test('renders the expected content', () => {
|
||||
const wrapper = mountWithContexts(
|
||||
<LaunchButton templateId={1}>{children}</LaunchButton>
|
||||
<LaunchButton resource={resource}>{children}</LaunchButton>
|
||||
);
|
||||
expect(wrapper).toHaveLength(1);
|
||||
});
|
||||
@ -33,7 +39,7 @@ describe('LaunchButton', () => {
|
||||
},
|
||||
});
|
||||
const wrapper = mountWithContexts(
|
||||
<LaunchButton templateId={1}>{children}</LaunchButton>,
|
||||
<LaunchButton resource={resource}>{children}</LaunchButton>,
|
||||
{
|
||||
context: {
|
||||
router: { history },
|
||||
@ -62,22 +68,17 @@ describe('LaunchButton', () => {
|
||||
})
|
||||
);
|
||||
const wrapper = mountWithContexts(
|
||||
<LaunchButton templateId={1}>{children}</LaunchButton>
|
||||
);
|
||||
const button = wrapper.find('button');
|
||||
button.prop('onClick')();
|
||||
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
|
||||
<LaunchButton resource={resource}>{children}</LaunchButton>
|
||||
);
|
||||
expect(wrapper.find('Modal').length).toBe(0);
|
||||
wrapper.find('button').prop('onClick')();
|
||||
await sleep(0);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Modal').length).toBe(1);
|
||||
wrapper.find('ModalBoxCloseButton').simulate('click');
|
||||
await sleep(0);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Modal').length).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@ -10,6 +10,7 @@ import { DetailList, Detail } from '@components/DetailList';
|
||||
import { ChipGroup, Chip, CredentialChip } from '@components/Chip';
|
||||
import { VariablesInput as _VariablesInput } from '@components/CodeMirrorInput';
|
||||
import ErrorDetail from '@components/ErrorDetail';
|
||||
import LaunchButton from '@components/LaunchButton';
|
||||
import { StatusIcon } from '@components/Sparkline';
|
||||
import { toTitleCase } from '@util/strings';
|
||||
import { Job } from '../../../types';
|
||||
@ -254,6 +255,16 @@ function JobDetail({ job, i18n, history }) {
|
||||
/>
|
||||
)}
|
||||
<ActionButtonWrapper>
|
||||
{job.type !== 'system_job' &&
|
||||
job.summary_fields.user_capabilities.start && (
|
||||
<LaunchButton resource={job} aria-label={i18n._(t`Relaunch`)}>
|
||||
{({ handleRelaunch }) => (
|
||||
<Button type="submit" onClick={handleRelaunch}>
|
||||
{i18n._(t`Relaunch`)}
|
||||
</Button>
|
||||
)}
|
||||
</LaunchButton>
|
||||
)}
|
||||
<Button
|
||||
variant="danger"
|
||||
aria-label={i18n._(t`Delete`)}
|
||||
|
||||
@ -1,19 +1,35 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
DataListItem,
|
||||
DataListItemRow,
|
||||
DataListItemCells,
|
||||
Tooltip,
|
||||
Button as PFButton,
|
||||
} from '@patternfly/react-core';
|
||||
import { RocketIcon } from '@patternfly/react-icons';
|
||||
import styled from 'styled-components';
|
||||
import DataListCell from '@components/DataListCell';
|
||||
import DataListCheck from '@components/DataListCheck';
|
||||
import LaunchButton from '@components/LaunchButton';
|
||||
import VerticalSeparator from '@components/VerticalSeparator';
|
||||
import { toTitleCase } from '@util/strings';
|
||||
import { JOB_TYPE_URL_SEGMENTS } from '../../../constants';
|
||||
|
||||
const StyledButton = styled(PFButton)`
|
||||
padding: 5px 8px;
|
||||
border: none;
|
||||
&:hover {
|
||||
background-color: #0066cc;
|
||||
color: white;
|
||||
}
|
||||
`;
|
||||
|
||||
class JobListItem extends Component {
|
||||
render() {
|
||||
const { job, isSelected, onSelect } = this.props;
|
||||
const { i18n, job, isSelected, onSelect } = this.props;
|
||||
|
||||
return (
|
||||
<DataListItem
|
||||
@ -41,6 +57,23 @@ class JobListItem extends Component {
|
||||
</DataListCell>,
|
||||
<DataListCell key="type">{toTitleCase(job.type)}</DataListCell>,
|
||||
<DataListCell key="finished">{job.finished}</DataListCell>,
|
||||
<DataListCell lastcolumn="true" key="relaunch">
|
||||
{job.type !== 'system_job' &&
|
||||
job.summary_fields.user_capabilities.start && (
|
||||
<Tooltip content={i18n._(t`Relaunch`)} position="top">
|
||||
<LaunchButton resource={job}>
|
||||
{({ handleRelaunch }) => (
|
||||
<StyledButton
|
||||
variant="plain"
|
||||
onClick={handleRelaunch}
|
||||
>
|
||||
<RocketIcon />
|
||||
</StyledButton>
|
||||
)}
|
||||
</LaunchButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</DataListCell>,
|
||||
]}
|
||||
/>
|
||||
</DataListItemRow>
|
||||
@ -49,4 +82,4 @@ class JobListItem extends Component {
|
||||
}
|
||||
}
|
||||
export { JobListItem as _JobListItem };
|
||||
export default JobListItem;
|
||||
export default withI18n()(JobListItem);
|
||||
|
||||
@ -16,6 +16,11 @@ describe('<JobListItem />', () => {
|
||||
id: 1,
|
||||
name: 'Job',
|
||||
type: 'project update',
|
||||
summary_fields: {
|
||||
user_capabilities: {
|
||||
start: true,
|
||||
},
|
||||
},
|
||||
}}
|
||||
detailUrl="/organization/1"
|
||||
isSelected
|
||||
|
||||
@ -280,14 +280,8 @@ class JobTemplateDetail extends Component {
|
||||
</Button>
|
||||
)}
|
||||
{canLaunch && (
|
||||
<LaunchButton
|
||||
variant="secondary"
|
||||
component={Link}
|
||||
to="/templates"
|
||||
templateId={template.id}
|
||||
aria-label={i18n._(t`Launch`)}
|
||||
>
|
||||
{handleLaunch => (
|
||||
<LaunchButton resource={template} aria-label={i18n._(t`Launch`)}>
|
||||
{({ handleLaunch }) => (
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="submit"
|
||||
|
||||
@ -63,12 +63,8 @@ class TemplateListItem extends Component {
|
||||
<DataListCell lastcolumn="true" key="launch">
|
||||
{canLaunch && template.type === 'job_template' && (
|
||||
<Tooltip content={i18n._(t`Launch`)} position="top">
|
||||
<LaunchButton
|
||||
component={Link}
|
||||
to="/templates"
|
||||
templateId={template.id}
|
||||
>
|
||||
{handleLaunch => (
|
||||
<LaunchButton resource={template}>
|
||||
{({ handleLaunch }) => (
|
||||
<StyledButton variant="plain" onClick={handleLaunch}>
|
||||
<RocketIcon />
|
||||
</StyledButton>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user