diff --git a/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx b/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx index 86df83f41e..a99a38cf69 100644 --- a/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx +++ b/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx @@ -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 }); } diff --git a/awx/ui_next/src/components/LaunchButton/LaunchButton.test.jsx b/awx/ui_next/src/components/LaunchButton/LaunchButton.test.jsx index 60a24054a2..b9bd3f7e53 100644 --- a/awx/ui_next/src/components/LaunchButton/LaunchButton.test.jsx +++ b/awx/ui_next/src/components/LaunchButton/LaunchButton.test.jsx @@ -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: [], }, }); diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx index 913fcfb648..1f7b5554a4 100644 --- a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx +++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateList.jsx @@ -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)} /> diff --git a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.test.jsx b/awx/ui_next/src/screens/Template/TemplateList/TemplateList.test.jsx index d80a244b27..9059bd9ba7 100644 --- a/awx/ui_next/src/screens/Template/TemplateList/TemplateList.test.jsx +++ b/awx/ui_next/src/screens/Template/TemplateList/TemplateList.test.jsx @@ -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('', () => { 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( - } - />, - { - 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', - }); - }); }); diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.jsx index 754d0765e3..2cac0a8eae 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/Visualizer.jsx @@ -431,6 +431,7 @@ function Visualizer({ template, i18n }) { {links.length > 0 ? ( diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerToolbar.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerToolbar.jsx index 9f88c11931..abdab2a40f 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerToolbar.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerToolbar.jsx @@ -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 (
@@ -92,9 +101,18 @@ function VisualizerToolbar({ i18n, onClose, onSave, template }) { > - - - + + {({ handleLaunch }) => ( + + + + )} + { onClose={close} onSave={save} template={template} + hasUnsavedChanges={false} /> @@ -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( + + + + + + ); + 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();