diff --git a/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx b/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx index a832a929f8..a8322d86c5 100644 --- a/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx +++ b/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx @@ -1,4 +1,4 @@ -import React, { Fragment } from 'react'; +import React, { Fragment, useState } from 'react'; import { withRouter } from 'react-router-dom'; import { number, shape } from 'prop-types'; import { withI18n } from '@lingui/react'; @@ -32,40 +32,12 @@ function canLaunchWithoutPrompt(launchData) { ); } -class LaunchButton extends React.Component { - static propTypes = { - resource: shape({ - id: number.isRequired, - }).isRequired, - }; - - constructor(props) { - super(props); - - this.state = { - showLaunchPrompt: false, - launchConfig: null, - launchError: false, - surveyConfig: null, - }; - - this.handleLaunch = this.handleLaunch.bind(this); - this.launchWithParams = this.launchWithParams.bind(this); - this.handleRelaunch = this.handleRelaunch.bind(this); - this.handleLaunchErrorClose = this.handleLaunchErrorClose.bind(this); - this.handlePromptErrorClose = this.handlePromptErrorClose.bind(this); - } - - handleLaunchErrorClose() { - this.setState({ launchError: null }); - } - - handlePromptErrorClose() { - this.setState({ showLaunchPrompt: false }); - } - - async handleLaunch() { - const { resource } = this.props; +function LaunchButton({ resource, i18n, children, history }) { + const [showLaunchPrompt, setShowLaunchPrompt] = useState(false); + const [launchConfig, setLaunchConfig] = useState(null); + const [surveyConfig, setSurveyConfig] = useState(null); + const [error, setError] = useState(null); + const handleLaunch = async () => { const readLaunch = resource.type === 'workflow_job_template' ? WorkflowJobTemplatesAPI.readLaunch(resource.id) @@ -75,33 +47,27 @@ class LaunchButton extends React.Component { ? WorkflowJobTemplatesAPI.readSurvey(resource.id) : JobTemplatesAPI.readSurvey(resource.id); try { - const { data: launchConfig } = await readLaunch; + const { data: launch } = await readLaunch; + setLaunchConfig(launch); - let surveyConfig = null; - - if (launchConfig.survey_enabled) { + if (launch.survey_enabled) { const { data } = await readSurvey; - surveyConfig = data; + setSurveyConfig(data); } - if (canLaunchWithoutPrompt(launchConfig)) { - this.launchWithParams({}); + if (canLaunchWithoutPrompt(launch)) { + launchWithParams({}); } else { - this.setState({ - showLaunchPrompt: true, - launchConfig, - surveyConfig, - }); + setShowLaunchPrompt(true); } } catch (err) { - this.setState({ launchError: err }); + setError(err); } - } + }; - async launchWithParams(params) { + const launchWithParams = async params => { try { - const { history, resource } = this.props; let jobPromise; if (resource.type === 'job_template') { @@ -117,13 +83,11 @@ class LaunchButton extends React.Component { const { data: job } = await jobPromise; history.push(`/jobs/${job.id}/output`); } catch (launchError) { - this.setState({ launchError }); + setError(launchError); } - } - - async handleRelaunch() { - const { history, resource } = this.props; + }; + const handleRelaunch = async () => { let readRelaunch; let relaunch; @@ -145,6 +109,7 @@ class LaunchButton extends React.Component { try { const { data: relaunchConfig } = await readRelaunch; + setLaunchConfig(relaunchConfig); if ( !relaunchConfig.passwords_needed_to_start || relaunchConfig.passwords_needed_to_start.length === 0 @@ -165,53 +130,47 @@ class LaunchButton extends React.Component { const { data: job } = await relaunch; history.push(`/jobs/${job.id}/output`); } else { - this.setState({ - showLaunchPrompt: true, - launchConfig: relaunchConfig, - }); + setShowLaunchPrompt(true); } } catch (err) { - this.setState({ launchError: err }); + setError(err); } - } + }; - render() { - const { - launchError, - showLaunchPrompt, - launchConfig, - surveyConfig, - } = this.state; - const { resource, i18n, children } = this.props; - return ( - - {children({ - handleLaunch: this.handleLaunch, - handleRelaunch: this.handleRelaunch, - })} - {launchError && ( - - {i18n._(t`Failed to launch job.`)} - - - )} - {showLaunchPrompt && ( - this.setState({ showLaunchPrompt: false })} - /> - )} - - ); - } + return ( + + {children({ + handleLaunch, + handleRelaunch, + })} + {error && ( + setError(null)} + > + {i18n._(t`Failed to launch job.`)} + + + )} + {showLaunchPrompt && ( + setShowLaunchPrompt(false)} + /> + )} + + ); } +LaunchButton.propTypes = { + resource: shape({ + id: number.isRequired, + }).isRequired, +}; + export default withI18n()(withRouter(LaunchButton)); diff --git a/awx/ui_next/src/components/LaunchButton/LaunchButton.test.jsx b/awx/ui_next/src/components/LaunchButton/LaunchButton.test.jsx index c84c7fde4d..4d4ce3ac2e 100644 --- a/awx/ui_next/src/components/LaunchButton/LaunchButton.test.jsx +++ b/awx/ui_next/src/components/LaunchButton/LaunchButton.test.jsx @@ -1,5 +1,6 @@ import React from 'react'; import { createMemoryHistory } from 'history'; +import { act } from 'react-dom/test-utils'; import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; import { sleep } from '../../../testUtils/testUtils'; @@ -69,7 +70,7 @@ describe('LaunchButton', () => { } ); const button = wrapper.find('button'); - button.prop('onClick')(); + await act(() => button.prop('onClick')()); expect(JobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1); await sleep(0); expect(JobTemplatesAPI.launch).toHaveBeenCalledWith(1, {}); @@ -106,7 +107,7 @@ describe('LaunchButton', () => { } ); const button = wrapper.find('button'); - button.prop('onClick')(); + await act(() => button.prop('onClick')()); expect(WorkflowJobTemplatesAPI.readLaunch).toHaveBeenCalledWith(1); await sleep(0); expect(WorkflowJobTemplatesAPI.launch).toHaveBeenCalledWith(1, {}); @@ -143,7 +144,7 @@ describe('LaunchButton', () => { } ); const button = wrapper.find('button'); - button.prop('onClick')(); + await act(() => button.prop('onClick')()); expect(JobsAPI.readRelaunch).toHaveBeenCalledWith(1); await sleep(0); expect(JobsAPI.relaunch).toHaveBeenCalledWith(1); @@ -180,7 +181,7 @@ describe('LaunchButton', () => { } ); const button = wrapper.find('button'); - button.prop('onClick')(); + await act(() => button.prop('onClick')()); expect(WorkflowJobsAPI.readRelaunch).toHaveBeenCalledWith(1); await sleep(0); expect(WorkflowJobsAPI.relaunch).toHaveBeenCalledWith(1); @@ -218,7 +219,7 @@ describe('LaunchButton', () => { } ); const button = wrapper.find('button'); - button.prop('onClick')(); + await act(() => button.prop('onClick')()); expect(ProjectsAPI.readLaunchUpdate).toHaveBeenCalledWith(5); await sleep(0); expect(ProjectsAPI.launchUpdate).toHaveBeenCalledWith(5); @@ -256,7 +257,7 @@ describe('LaunchButton', () => { } ); const button = wrapper.find('button'); - button.prop('onClick')(); + await act(() => button.prop('onClick')()); expect(InventorySourcesAPI.readLaunchUpdate).toHaveBeenCalledWith(5); await sleep(0); expect(InventorySourcesAPI.launchUpdate).toHaveBeenCalledWith(5); @@ -280,7 +281,7 @@ describe('LaunchButton', () => { }) ); expect(wrapper.find('Modal').length).toBe(0); - wrapper.find('button').prop('onClick')(); + await act(() => wrapper.find('button').prop('onClick')()); await sleep(0); wrapper.update(); expect(wrapper.find('Modal').length).toBe(1); diff --git a/awx/ui_next/src/components/Lookup/Lookup.jsx b/awx/ui_next/src/components/Lookup/Lookup.jsx index d6dea5e49e..197981678d 100644 --- a/awx/ui_next/src/components/Lookup/Lookup.jsx +++ b/awx/ui_next/src/components/Lookup/Lookup.jsx @@ -123,6 +123,7 @@ function Lookup(props) { + {i18n._(t`Select`)} , - , ]} diff --git a/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx b/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx index 97c6220d0c..ac974c44ff 100644 --- a/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx +++ b/awx/ui_next/src/screens/Job/JobOutput/JobOutput.jsx @@ -1,6 +1,6 @@ import React, { Component, Fragment } from 'react'; import { withRouter } from 'react-router-dom'; -import { withI18n } from '@lingui/react'; +import { I18n } from '@lingui/react'; import { t } from '@lingui/macro'; import styled from 'styled-components'; import { @@ -578,7 +578,7 @@ class JobOutput extends Component { } render() { - const { job, i18n } = this.props; + const { job } = this.props; const { contentError, @@ -666,64 +666,72 @@ class JobOutput extends Component { {showCancelPrompt && ['pending', 'waiting', 'running'].includes(jobStatus) && ( - + {({ i18n }) => ( + + {i18n._(t`Cancel job`)} + , + , + ]} > - {i18n._(t`Cancel job`)} - , - , - ]} - > - {i18n._( - t`Are you sure you want to submit the request to cancel this job?` + {i18n._( + t`Are you sure you want to submit the request to cancel this job?` + )} + )} - + )} {cancelError && ( - <> - this.setState({ cancelError: null })} - title={i18n._(t`Job Cancel Error`)} - label={i18n._(t`Job Cancel Error`)} - > - - - + + {({ i18n }) => ( + this.setState({ cancelError: null })} + title={i18n._(t`Job Cancel Error`)} + label={i18n._(t`Job Cancel Error`)} + > + + + )} + )} {deletionError && ( - <> - this.setState({ deletionError: null })} - title={i18n._(t`Job Delete Error`)} - label={i18n._(t`Job Delete Error`)} - > - - - + + {({ i18n }) => ( + this.setState({ deletionError: null })} + title={i18n._(t`Job Delete Error`)} + label={i18n._(t`Job Delete Error`)} + > + + + )} + )} ); @@ -731,4 +739,4 @@ class JobOutput extends Component { } export { JobOutput as _JobOutput }; -export default withI18n()(withRouter(JobOutput)); +export default withRouter(JobOutput);