mirror of
https://github.com/ansible/awx.git
synced 2026-01-13 19:10:07 -03:30
Add launch button to workflow visualizer
This commit is contained in:
parent
d941f11ccd
commit
abfeb735f0
@ -16,6 +16,18 @@ import {
|
||||
WorkflowJobTemplatesAPI,
|
||||
} from '@api';
|
||||
|
||||
function canLaunchWithoutPrompt(launchData) {
|
||||
return (
|
||||
launchData.can_start_without_user_input &&
|
||||
!launchData.ask_inventory_on_launch &&
|
||||
!launchData.ask_variables_on_launch &&
|
||||
!launchData.ask_limit_on_launch &&
|
||||
!launchData.ask_scm_branch_on_launch &&
|
||||
!launchData.survey_enabled &&
|
||||
launchData.variables_needed_to_start.length === 0
|
||||
);
|
||||
}
|
||||
|
||||
class LaunchButton extends React.Component {
|
||||
static propTypes = {
|
||||
resource: shape({
|
||||
@ -47,19 +59,23 @@ class LaunchButton extends React.Component {
|
||||
|
||||
async handleLaunch() {
|
||||
const { history, resource } = this.props;
|
||||
|
||||
const readLaunch =
|
||||
resource.type === 'workflow_job_template'
|
||||
? WorkflowJobTemplatesAPI.readLaunch(resource.id)
|
||||
: JobTemplatesAPI.readLaunch(resource.id);
|
||||
|
||||
const launchJob =
|
||||
resource.type === 'workflow_job_template'
|
||||
? WorkflowJobTemplatesAPI.launch(resource.id)
|
||||
: JobTemplatesAPI.launch(resource.id);
|
||||
|
||||
try {
|
||||
const { data: launchConfig } = await readLaunch;
|
||||
|
||||
if (launchConfig.can_start_without_user_input) {
|
||||
if (canLaunchWithoutPrompt(launchConfig)) {
|
||||
const { data: job } = await launchJob;
|
||||
|
||||
history.push(
|
||||
`/${
|
||||
resource.type === 'workflow_job_template' ? 'jobs/workflow' : 'jobs'
|
||||
@ -107,7 +123,7 @@ class LaunchButton extends React.Component {
|
||||
relaunchConfig.passwords_needed_to_start.length === 0
|
||||
) {
|
||||
const { data: job } = await relaunch;
|
||||
history.push(`/jobs/${job.id}`);
|
||||
history.push(`/jobs/${job.id}/output`);
|
||||
} else {
|
||||
this.setState({ promptError: true });
|
||||
}
|
||||
|
||||
@ -13,6 +13,12 @@ describe('LaunchButton', () => {
|
||||
JobTemplatesAPI.readLaunch.mockResolvedValue({
|
||||
data: {
|
||||
can_start_without_user_input: true,
|
||||
ask_inventory_on_launch: false,
|
||||
ask_variables_on_launch: false,
|
||||
ask_limit_on_launch: false,
|
||||
ask_scm_branch_on_launch: false,
|
||||
survey_enabled: false,
|
||||
variables_needed_to_start: [],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useParams, useLocation } from 'react-router-dom';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Card, PageSection } from '@patternfly/react-core';
|
||||
@ -31,7 +31,6 @@ const QS_CONFIG = getQSConfig('template', {
|
||||
});
|
||||
|
||||
function TemplateList({ i18n }) {
|
||||
const { id: projectId } = useParams();
|
||||
const location = useLocation();
|
||||
|
||||
const [selected, setSelected] = useState([]);
|
||||
@ -44,9 +43,6 @@ function TemplateList({ i18n }) {
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const params = parseQueryString(QS_CONFIG, location.search);
|
||||
if (location.pathname.startsWith('/projects') && projectId) {
|
||||
params.jobtemplate__project = projectId;
|
||||
}
|
||||
const results = await Promise.all([
|
||||
UnifiedJobTemplatesAPI.read(params),
|
||||
JobTemplatesAPI.readOptions(),
|
||||
@ -58,7 +54,7 @@ function TemplateList({ i18n }) {
|
||||
jtActions: results[1].data.actions,
|
||||
wfjtActions: results[2].data.actions,
|
||||
};
|
||||
}, [location, projectId]),
|
||||
}, [location]),
|
||||
{
|
||||
templates: [],
|
||||
count: 0,
|
||||
@ -228,7 +224,7 @@ function TemplateList({ i18n }) {
|
||||
key={template.id}
|
||||
value={template.name}
|
||||
template={template}
|
||||
detailUrl={`${location.pathname}/${template.type}/${template.id}`}
|
||||
detailUrl={`/templates/${template.type}/${template.id}`}
|
||||
onSelect={() => handleSelect(template)}
|
||||
isSelected={selected.some(row => row.id === template.id)}
|
||||
/>
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { Route } from 'react-router-dom';
|
||||
import {
|
||||
JobTemplatesAPI,
|
||||
UnifiedJobTemplatesAPI,
|
||||
@ -308,36 +306,4 @@ describe('<TemplateList />', () => {
|
||||
el => el.props().isOpen === true && el.props().title === 'Error!'
|
||||
);
|
||||
});
|
||||
test('Calls API with jobtemplate__project id', async () => {
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/projects/6/job_templates'],
|
||||
});
|
||||
const wrapper = mountWithContexts(
|
||||
<Route
|
||||
path="/projects/:id/job_templates"
|
||||
component={() => <TemplateList />}
|
||||
/>,
|
||||
{
|
||||
context: {
|
||||
router: {
|
||||
history,
|
||||
route: {
|
||||
location: history.location,
|
||||
match: { params: { id: 6 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
await act(async () => {
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 1);
|
||||
});
|
||||
expect(UnifiedJobTemplatesAPI.read).toBeCalledWith({
|
||||
jobtemplate__project: '6',
|
||||
order_by: 'name',
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
type: 'job_template,workflow_job_template',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -431,6 +431,7 @@ function Visualizer({ template, i18n }) {
|
||||
<VisualizerToolbar
|
||||
onClose={handleVisualizerClose}
|
||||
onSave={handleVisualizerSave}
|
||||
hasUnsavedChanges={unsavedChanges}
|
||||
template={template}
|
||||
/>
|
||||
{links.length > 0 ? (
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
} from '@contexts/Workflow';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { func, shape } from 'prop-types';
|
||||
import { bool, func, shape } from 'prop-types';
|
||||
import {
|
||||
Badge as PFBadge,
|
||||
Button,
|
||||
@ -20,6 +20,7 @@ import {
|
||||
TrashAltIcon,
|
||||
WrenchIcon,
|
||||
} from '@patternfly/react-icons';
|
||||
import LaunchButton from '@components/LaunchButton';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Badge = styled(PFBadge)`
|
||||
@ -45,12 +46,20 @@ const ActionButton = styled(Button)`
|
||||
`;
|
||||
ActionButton.displayName = 'ActionButton';
|
||||
|
||||
function VisualizerToolbar({ i18n, onClose, onSave, template }) {
|
||||
function VisualizerToolbar({
|
||||
i18n,
|
||||
onClose,
|
||||
onSave,
|
||||
template,
|
||||
hasUnsavedChanges,
|
||||
}) {
|
||||
const dispatch = useContext(WorkflowDispatchContext);
|
||||
|
||||
const { nodes, showLegend, showTools } = useContext(WorkflowStateContext);
|
||||
|
||||
const totalNodes = nodes.reduce((n, node) => n + !node.isDeleted, 0) - 1;
|
||||
const canLaunch =
|
||||
template.summary_fields?.user_capabilities?.start && !hasUnsavedChanges;
|
||||
|
||||
return (
|
||||
<div id="visualizer-toolbar">
|
||||
@ -92,9 +101,18 @@ function VisualizerToolbar({ i18n, onClose, onSave, template }) {
|
||||
>
|
||||
<BookIcon />
|
||||
</ActionButton>
|
||||
<ActionButton id="visualizer-launch" variant="plain" isDisabled>
|
||||
<RocketIcon />
|
||||
</ActionButton>
|
||||
<LaunchButton resource={template} aria-label={i18n._(t`Launch`)}>
|
||||
{({ handleLaunch }) => (
|
||||
<ActionButton
|
||||
id="visualizer-launch"
|
||||
variant="plain"
|
||||
isDisabled={!canLaunch}
|
||||
onClick={handleLaunch}
|
||||
>
|
||||
<RocketIcon />
|
||||
</ActionButton>
|
||||
)}
|
||||
</LaunchButton>
|
||||
<Tooltip content={i18n._(t`Delete All Nodes`)} position="bottom">
|
||||
<ActionButton
|
||||
id="visualizer-delete-all"
|
||||
@ -138,6 +156,7 @@ VisualizerToolbar.propTypes = {
|
||||
onClose: func.isRequired,
|
||||
onSave: func.isRequired,
|
||||
template: shape().isRequired,
|
||||
hasUnsavedChanges: bool.isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(VisualizerToolbar);
|
||||
|
||||
@ -13,6 +13,11 @@ const save = jest.fn();
|
||||
const template = {
|
||||
id: 1,
|
||||
name: 'Test JT',
|
||||
summary_fields: {
|
||||
user_capabilities: {
|
||||
start: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
const workflowContext = {
|
||||
nodes: [],
|
||||
@ -41,6 +46,7 @@ describe('VisualizerToolbar', () => {
|
||||
onClose={close}
|
||||
onSave={save}
|
||||
template={template}
|
||||
hasUnsavedChanges={false}
|
||||
/>
|
||||
</WorkflowStateContext.Provider>
|
||||
</WorkflowDispatchContext.Provider>
|
||||
@ -82,6 +88,30 @@ describe('VisualizerToolbar', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Launch button should be disabled when there are unsaved changes', () => {
|
||||
expect(wrapper.find('LaunchButton button').prop('disabled')).toEqual(false);
|
||||
const nodes = [
|
||||
{
|
||||
id: 1,
|
||||
},
|
||||
];
|
||||
const disabledToolbar = mountWithContexts(
|
||||
<WorkflowDispatchContext.Provider value={dispatch}>
|
||||
<WorkflowStateContext.Provider value={{ ...workflowContext, nodes }}>
|
||||
<VisualizerToolbar
|
||||
onClose={close}
|
||||
onSave={save}
|
||||
template={template}
|
||||
hasUnsavedChanges
|
||||
/>
|
||||
</WorkflowStateContext.Provider>
|
||||
</WorkflowDispatchContext.Provider>
|
||||
);
|
||||
expect(
|
||||
disabledToolbar.find('LaunchButton button').prop('disabled')
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
test('Save button calls expected function', () => {
|
||||
wrapper.find('button[aria-label="Save"]').simulate('click');
|
||||
expect(save).toHaveBeenCalled();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user