mirror of
https://github.com/ansible/awx.git
synced 2026-02-23 05:55:59 -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:
@@ -2,6 +2,7 @@ import AdHocCommands from './models/AdHocCommands';
|
|||||||
import Config from './models/Config';
|
import Config from './models/Config';
|
||||||
import InstanceGroups from './models/InstanceGroups';
|
import InstanceGroups from './models/InstanceGroups';
|
||||||
import Inventories from './models/Inventories';
|
import Inventories from './models/Inventories';
|
||||||
|
import InventorySources from './models/InventorySources';
|
||||||
import InventoryUpdates from './models/InventoryUpdates';
|
import InventoryUpdates from './models/InventoryUpdates';
|
||||||
import JobTemplates from './models/JobTemplates';
|
import JobTemplates from './models/JobTemplates';
|
||||||
import Jobs from './models/Jobs';
|
import Jobs from './models/Jobs';
|
||||||
@@ -23,6 +24,7 @@ const AdHocCommandsAPI = new AdHocCommands();
|
|||||||
const ConfigAPI = new Config();
|
const ConfigAPI = new Config();
|
||||||
const InstanceGroupsAPI = new InstanceGroups();
|
const InstanceGroupsAPI = new InstanceGroups();
|
||||||
const InventoriesAPI = new Inventories();
|
const InventoriesAPI = new Inventories();
|
||||||
|
const InventorySourcesAPI = new InventorySources();
|
||||||
const InventoryUpdatesAPI = new InventoryUpdates();
|
const InventoryUpdatesAPI = new InventoryUpdates();
|
||||||
const JobTemplatesAPI = new JobTemplates();
|
const JobTemplatesAPI = new JobTemplates();
|
||||||
const JobsAPI = new Jobs();
|
const JobsAPI = new Jobs();
|
||||||
@@ -45,6 +47,7 @@ export {
|
|||||||
ConfigAPI,
|
ConfigAPI,
|
||||||
InstanceGroupsAPI,
|
InstanceGroupsAPI,
|
||||||
InventoriesAPI,
|
InventoriesAPI,
|
||||||
|
InventorySourcesAPI,
|
||||||
InventoryUpdatesAPI,
|
InventoryUpdatesAPI,
|
||||||
JobTemplatesAPI,
|
JobTemplatesAPI,
|
||||||
JobsAPI,
|
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 Base from '../Base';
|
||||||
|
import RelaunchMixin from '../mixins/Relaunch.mixin';
|
||||||
|
|
||||||
class AdHocCommands extends Base {
|
class AdHocCommands extends RelaunchMixin(Base) {
|
||||||
constructor(http) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/ad_hoc_commands/';
|
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 Base from '../Base';
|
||||||
|
import LaunchUpdateMixin from '../mixins/LaunchUpdate.mixin';
|
||||||
|
|
||||||
class InventoryUpdates extends Base {
|
class InventoryUpdates extends LaunchUpdateMixin(Base) {
|
||||||
constructor(http) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/inventory_updates/';
|
this.baseUrl = '/api/v2/inventory_updates/';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Base from '../Base';
|
import Base from '../Base';
|
||||||
|
import RelaunchMixin from '../mixins/Relaunch.mixin';
|
||||||
|
|
||||||
const BASE_URLS = {
|
const BASE_URLS = {
|
||||||
playbook: '/jobs/',
|
playbook: '/jobs/',
|
||||||
@@ -9,7 +10,7 @@ const BASE_URLS = {
|
|||||||
workflow: '/workflow_jobs/',
|
workflow: '/workflow_jobs/',
|
||||||
};
|
};
|
||||||
|
|
||||||
class Jobs extends Base {
|
class Jobs extends RelaunchMixin(Base) {
|
||||||
constructor(http) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/jobs/';
|
this.baseUrl = '/api/v2/jobs/';
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Base from '../Base';
|
import Base from '../Base';
|
||||||
|
import LaunchUpdateMixin from '../mixins/LaunchUpdate.mixin';
|
||||||
|
|
||||||
class Projects extends Base {
|
class Projects extends LaunchUpdateMixin(Base) {
|
||||||
constructor(http) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/projects/';
|
this.baseUrl = '/api/v2/projects/';
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Base from '../Base';
|
import Base from '../Base';
|
||||||
|
import RelaunchMixin from '../mixins/Relaunch.mixin';
|
||||||
|
|
||||||
class WorkflowJobs extends Base {
|
class WorkflowJobs extends RelaunchMixin(Base) {
|
||||||
constructor(http) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/workflow_jobs/';
|
this.baseUrl = '/api/v2/workflow_jobs/';
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { number } from 'prop-types';
|
import { number, shape } from 'prop-types';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
import AlertModal from '@components/AlertModal';
|
import AlertModal from '@components/AlertModal';
|
||||||
import ErrorDetail from '@components/ErrorDetail';
|
import ErrorDetail from '@components/ErrorDetail';
|
||||||
import { JobTemplatesAPI } from '@api';
|
import {
|
||||||
|
AdHocCommandsAPI,
|
||||||
|
InventorySourcesAPI,
|
||||||
|
JobsAPI,
|
||||||
|
JobTemplatesAPI,
|
||||||
|
ProjectsAPI,
|
||||||
|
WorkflowJobsAPI,
|
||||||
|
} from '@api';
|
||||||
|
|
||||||
class LaunchButton extends React.Component {
|
class LaunchButton extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
templateId: number.isRequired,
|
resource: shape({
|
||||||
|
id: number.isRequired,
|
||||||
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -22,6 +31,7 @@ class LaunchButton extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.handleLaunch = this.handleLaunch.bind(this);
|
this.handleLaunch = this.handleLaunch.bind(this);
|
||||||
|
this.handleRelaunch = this.handleRelaunch.bind(this);
|
||||||
this.handleLaunchErrorClose = this.handleLaunchErrorClose.bind(this);
|
this.handleLaunchErrorClose = this.handleLaunchErrorClose.bind(this);
|
||||||
this.handlePromptErrorClose = this.handlePromptErrorClose.bind(this);
|
this.handlePromptErrorClose = this.handlePromptErrorClose.bind(this);
|
||||||
}
|
}
|
||||||
@@ -35,13 +45,56 @@ class LaunchButton extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleLaunch() {
|
async handleLaunch() {
|
||||||
const { history, templateId } = this.props;
|
const { history, resource } = this.props;
|
||||||
try {
|
try {
|
||||||
const { data: launchConfig } = await JobTemplatesAPI.readLaunch(
|
const { data: launchConfig } = await JobTemplatesAPI.readLaunch(
|
||||||
templateId
|
resource.id
|
||||||
);
|
);
|
||||||
if (launchConfig.can_start_without_user_input) {
|
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`);
|
history.push(`/jobs/${job.id}/details`);
|
||||||
} else {
|
} else {
|
||||||
this.setState({ promptError: true });
|
this.setState({ promptError: true });
|
||||||
@@ -56,26 +109,33 @@ class LaunchButton extends React.Component {
|
|||||||
const { i18n, children } = this.props;
|
const { i18n, children } = this.props;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{children(this.handleLaunch)}
|
{children({
|
||||||
<AlertModal
|
handleLaunch: this.handleLaunch,
|
||||||
isOpen={launchError}
|
handleRelaunch: this.handleRelaunch,
|
||||||
variant="danger"
|
})}
|
||||||
title={i18n._(t`Error!`)}
|
{launchError && (
|
||||||
onClose={this.handleLaunchErrorClose}
|
<AlertModal
|
||||||
>
|
isOpen={launchError}
|
||||||
{i18n._(t`Failed to launch job.`)}
|
variant="danger"
|
||||||
<ErrorDetail error={launchError} />
|
title={i18n._(t`Error!`)}
|
||||||
</AlertModal>
|
onClose={this.handleLaunchErrorClose}
|
||||||
<AlertModal
|
>
|
||||||
isOpen={promptError}
|
{i18n._(t`Failed to launch job.`)}
|
||||||
variant="info"
|
<ErrorDetail error={launchError} />
|
||||||
title={i18n._(t`Attention!`)}
|
</AlertModal>
|
||||||
onClose={this.handlePromptErrorClose}
|
)}
|
||||||
>
|
{promptError && (
|
||||||
{i18n._(
|
<AlertModal
|
||||||
t`Launching jobs with promptable fields is not supported at this time.`
|
isOpen={promptError}
|
||||||
)}
|
variant="info"
|
||||||
</AlertModal>
|
title={i18n._(t`Attention!`)}
|
||||||
|
onClose={this.handlePromptErrorClose}
|
||||||
|
>
|
||||||
|
{i18n._(
|
||||||
|
t`Launching jobs with promptable fields is not supported at this time.`
|
||||||
|
)}
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||||
import { sleep } from '@testUtils/testUtils';
|
import { sleep } from '@testUtils/testUtils';
|
||||||
|
|
||||||
import LaunchButton from './LaunchButton';
|
import LaunchButton from './LaunchButton';
|
||||||
@@ -13,13 +13,19 @@ describe('LaunchButton', () => {
|
|||||||
can_start_without_user_input: true,
|
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', () => {
|
test('renders the expected content', () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<LaunchButton templateId={1}>{children}</LaunchButton>
|
<LaunchButton resource={resource}>{children}</LaunchButton>
|
||||||
);
|
);
|
||||||
expect(wrapper).toHaveLength(1);
|
expect(wrapper).toHaveLength(1);
|
||||||
});
|
});
|
||||||
@@ -33,7 +39,7 @@ describe('LaunchButton', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<LaunchButton templateId={1}>{children}</LaunchButton>,
|
<LaunchButton resource={resource}>{children}</LaunchButton>,
|
||||||
{
|
{
|
||||||
context: {
|
context: {
|
||||||
router: { history },
|
router: { history },
|
||||||
@@ -62,22 +68,17 @@ describe('LaunchButton', () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<LaunchButton templateId={1}>{children}</LaunchButton>
|
<LaunchButton resource={resource}>{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
|
|
||||||
);
|
);
|
||||||
|
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();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { DetailList, Detail } from '@components/DetailList';
|
|||||||
import { ChipGroup, Chip, CredentialChip } from '@components/Chip';
|
import { ChipGroup, Chip, CredentialChip } from '@components/Chip';
|
||||||
import { VariablesInput as _VariablesInput } from '@components/CodeMirrorInput';
|
import { VariablesInput as _VariablesInput } from '@components/CodeMirrorInput';
|
||||||
import ErrorDetail from '@components/ErrorDetail';
|
import ErrorDetail from '@components/ErrorDetail';
|
||||||
|
import LaunchButton from '@components/LaunchButton';
|
||||||
import { StatusIcon } from '@components/Sparkline';
|
import { StatusIcon } from '@components/Sparkline';
|
||||||
import { toTitleCase } from '@util/strings';
|
import { toTitleCase } from '@util/strings';
|
||||||
import { Job } from '../../../types';
|
import { Job } from '../../../types';
|
||||||
@@ -254,6 +255,16 @@ function JobDetail({ job, i18n, history }) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ActionButtonWrapper>
|
<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
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
aria-label={i18n._(t`Delete`)}
|
aria-label={i18n._(t`Delete`)}
|
||||||
|
|||||||
@@ -1,19 +1,35 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
import {
|
import {
|
||||||
DataListItem,
|
DataListItem,
|
||||||
DataListItemRow,
|
DataListItemRow,
|
||||||
DataListItemCells,
|
DataListItemCells,
|
||||||
|
Tooltip,
|
||||||
|
Button as PFButton,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
import { RocketIcon } from '@patternfly/react-icons';
|
||||||
|
import styled from 'styled-components';
|
||||||
import DataListCell from '@components/DataListCell';
|
import DataListCell from '@components/DataListCell';
|
||||||
import DataListCheck from '@components/DataListCheck';
|
import DataListCheck from '@components/DataListCheck';
|
||||||
|
import LaunchButton from '@components/LaunchButton';
|
||||||
import VerticalSeparator from '@components/VerticalSeparator';
|
import VerticalSeparator from '@components/VerticalSeparator';
|
||||||
import { toTitleCase } from '@util/strings';
|
import { toTitleCase } from '@util/strings';
|
||||||
import { JOB_TYPE_URL_SEGMENTS } from '../../../constants';
|
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 {
|
class JobListItem extends Component {
|
||||||
render() {
|
render() {
|
||||||
const { job, isSelected, onSelect } = this.props;
|
const { i18n, job, isSelected, onSelect } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataListItem
|
<DataListItem
|
||||||
@@ -41,6 +57,23 @@ class JobListItem extends Component {
|
|||||||
</DataListCell>,
|
</DataListCell>,
|
||||||
<DataListCell key="type">{toTitleCase(job.type)}</DataListCell>,
|
<DataListCell key="type">{toTitleCase(job.type)}</DataListCell>,
|
||||||
<DataListCell key="finished">{job.finished}</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>
|
</DataListItemRow>
|
||||||
@@ -49,4 +82,4 @@ class JobListItem extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
export { JobListItem as _JobListItem };
|
export { JobListItem as _JobListItem };
|
||||||
export default JobListItem;
|
export default withI18n()(JobListItem);
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ describe('<JobListItem />', () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
name: 'Job',
|
name: 'Job',
|
||||||
type: 'project update',
|
type: 'project update',
|
||||||
|
summary_fields: {
|
||||||
|
user_capabilities: {
|
||||||
|
start: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
detailUrl="/organization/1"
|
detailUrl="/organization/1"
|
||||||
isSelected
|
isSelected
|
||||||
|
|||||||
@@ -280,14 +280,8 @@ class JobTemplateDetail extends Component {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{canLaunch && (
|
{canLaunch && (
|
||||||
<LaunchButton
|
<LaunchButton resource={template} aria-label={i18n._(t`Launch`)}>
|
||||||
variant="secondary"
|
{({ handleLaunch }) => (
|
||||||
component={Link}
|
|
||||||
to="/templates"
|
|
||||||
templateId={template.id}
|
|
||||||
aria-label={i18n._(t`Launch`)}
|
|
||||||
>
|
|
||||||
{handleLaunch => (
|
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -63,12 +63,8 @@ class TemplateListItem extends Component {
|
|||||||
<DataListCell lastcolumn="true" key="launch">
|
<DataListCell lastcolumn="true" key="launch">
|
||||||
{canLaunch && template.type === 'job_template' && (
|
{canLaunch && template.type === 'job_template' && (
|
||||||
<Tooltip content={i18n._(t`Launch`)} position="top">
|
<Tooltip content={i18n._(t`Launch`)} position="top">
|
||||||
<LaunchButton
|
<LaunchButton resource={template}>
|
||||||
component={Link}
|
{({ handleLaunch }) => (
|
||||||
to="/templates"
|
|
||||||
templateId={template.id}
|
|
||||||
>
|
|
||||||
{handleLaunch => (
|
|
||||||
<StyledButton variant="plain" onClick={handleLaunch}>
|
<StyledButton variant="plain" onClick={handleLaunch}>
|
||||||
<RocketIcon />
|
<RocketIcon />
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
|
|||||||
Reference in New Issue
Block a user