diff --git a/awx/ui_next/src/components/JobList/JobListItem.jsx b/awx/ui_next/src/components/JobList/JobListItem.jsx
index 62b51a551d..0bd31dce77 100644
--- a/awx/ui_next/src/components/JobList/JobListItem.jsx
+++ b/awx/ui_next/src/components/JobList/JobListItem.jsx
@@ -91,18 +91,22 @@ function JobListItem({
>
{job.status === 'failed' && job.type === 'job' ? (
- {({ handleRelaunch }) => (
-
+ {({ handleRelaunch, isLaunching }) => (
+
)}
) : (
- {({ handleRelaunch }) => (
+ {({ handleRelaunch, isLaunching }) => (
diff --git a/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx b/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx
index e846c5fbd8..ab292ee976 100644
--- a/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx
+++ b/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx
@@ -36,9 +36,12 @@ function LaunchButton({ resource, i18n, children, history }) {
const [showLaunchPrompt, setShowLaunchPrompt] = useState(false);
const [launchConfig, setLaunchConfig] = useState(null);
const [surveyConfig, setSurveyConfig] = useState(null);
+ const [isLaunching, setIsLaunching] = useState(false);
const [resourceCredentials, setResourceCredentials] = useState([]);
const [error, setError] = useState(null);
+
const handleLaunch = async () => {
+ setIsLaunching(true);
const readLaunch =
resource.type === 'workflow_job_template'
? WorkflowJobTemplatesAPI.readLaunch(resource.id)
@@ -96,6 +99,8 @@ function LaunchButton({ resource, i18n, children, history }) {
history.push(`/jobs/${job.id}/output`);
} catch (launchError) {
setError(launchError);
+ } finally {
+ setIsLaunching(false);
}
};
@@ -103,6 +108,7 @@ function LaunchButton({ resource, i18n, children, history }) {
let readRelaunch;
let relaunch;
+ setIsLaunching(true);
if (resource.type === 'inventory_update') {
// We'll need to handle the scenario where the src no longer exists
readRelaunch = InventorySourcesAPI.readLaunchUpdate(
@@ -146,6 +152,8 @@ function LaunchButton({ resource, i18n, children, history }) {
}
} catch (err) {
setError(err);
+ } finally {
+ setIsLaunching(false);
}
};
@@ -154,6 +162,7 @@ function LaunchButton({ resource, i18n, children, history }) {
{children({
handleLaunch,
handleRelaunch,
+ isLaunching,
})}
{error && (
{
expect(history.location.pathname).toEqual('/jobs/9000/output');
});
+ test('should disable button to prevent duplicate clicks', async () => {
+ WorkflowJobTemplatesAPI.readLaunch.mockResolvedValue({
+ data: {
+ can_start_without_user_input: true,
+ },
+ });
+ const history = createMemoryHistory({
+ initialEntries: ['/jobs/9000'],
+ });
+ WorkflowJobTemplatesAPI.launch.mockImplementation(async () => {
+ // return asynchronously so isLaunching isn't set back to false in the
+ // same tick
+ await sleep(10);
+ return {
+ data: {
+ id: 9000,
+ },
+ };
+ });
+ const wrapper = mountWithContexts(
+
+ {({ handleLaunch, isLaunching }) => (
+
+ )}
+ ,
+ {
+ context: {
+ router: { history },
+ },
+ }
+ );
+ const button = wrapper.find('button');
+ await act(() => button.prop('onClick')());
+ wrapper.update();
+
+ expect(wrapper.find('button').prop('disabled')).toEqual(true);
+ });
+
test('should relaunch job correctly', async () => {
JobsAPI.readRelaunch.mockResolvedValue({
data: {
diff --git a/awx/ui_next/src/components/LaunchButton/ReLaunchDropDown.jsx b/awx/ui_next/src/components/LaunchButton/ReLaunchDropDown.jsx
index 911c5390b8..fc0b9114f6 100644
--- a/awx/ui_next/src/components/LaunchButton/ReLaunchDropDown.jsx
+++ b/awx/ui_next/src/components/LaunchButton/ReLaunchDropDown.jsx
@@ -11,11 +11,17 @@ import {
} from '@patternfly/react-core';
import { RocketIcon } from '@patternfly/react-icons';
-function ReLaunchDropDown({ isPrimary = false, handleRelaunch, i18n, ouiaId }) {
- const [isOpen, setIsOPen] = useState(false);
+function ReLaunchDropDown({
+ isPrimary = false,
+ handleRelaunch,
+ isLaunching,
+ i18n,
+ ouiaId,
+}) {
+ const [isOpen, setIsOpen] = useState(false);
const onToggle = () => {
- setIsOPen(prev => !prev);
+ setIsOpen(prev => !prev);
};
const dropdownItems = [
@@ -35,6 +41,7 @@ function ReLaunchDropDown({ isPrimary = false, handleRelaunch, i18n, ouiaId }) {
onClick={() => {
handleRelaunch({ hosts: 'all' });
}}
+ isDisabled={isLaunching}
>
{i18n._(t`All`)}
,
@@ -46,6 +53,7 @@ function ReLaunchDropDown({ isPrimary = false, handleRelaunch, i18n, ouiaId }) {
onClick={() => {
handleRelaunch({ hosts: 'failed' });
}}
+ isDisabled={isLaunching}
>
{i18n._(t`Failed hosts`)}
,
diff --git a/awx/ui_next/src/components/TemplateList/TemplateListItem.jsx b/awx/ui_next/src/components/TemplateList/TemplateListItem.jsx
index ca935c6f7a..83f386beeb 100644
--- a/awx/ui_next/src/components/TemplateList/TemplateListItem.jsx
+++ b/awx/ui_next/src/components/TemplateList/TemplateListItem.jsx
@@ -176,11 +176,11 @@ function TemplateListItem({
tooltip={i18n._(t`Launch Template`)}
>
- {({ handleLaunch }) => (
+ {({ handleLaunch, isLaunching }) => (
) : (
- {({ handleRelaunch }) => (
+ {({ handleRelaunch, isLaunching }) => (
diff --git a/awx/ui_next/src/screens/Job/JobOutput/shared/OutputToolbar.jsx b/awx/ui_next/src/screens/Job/JobOutput/shared/OutputToolbar.jsx
index 03faba1faa..73d667d1a6 100644
--- a/awx/ui_next/src/screens/Job/JobOutput/shared/OutputToolbar.jsx
+++ b/awx/ui_next/src/screens/Job/JobOutput/shared/OutputToolbar.jsx
@@ -144,21 +144,23 @@ const OutputToolbar = ({
>
{job.status === 'failed' && job.type === 'job' ? (
- {({ handleRelaunch }) => (
+ {({ handleRelaunch, isLaunching }) => (
)}
) : (
- {({ handleRelaunch }) => (
+ {({ handleRelaunch, isLaunching }) => (
diff --git a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx
index 9f93620f56..0f150e7bb7 100644
--- a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx
+++ b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx
@@ -116,12 +116,13 @@ function ProjectJobTemplateListItem({
{canLaunch && template.type === 'job_template' && (
- {({ handleLaunch }) => (
+ {({ handleLaunch, isLaunching }) => (
diff --git a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx
index a003dcc9d5..242092cc45 100644
--- a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx
+++ b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx
@@ -387,12 +387,13 @@ function JobTemplateDetail({ i18n, template }) {
)}
{canLaunch && (
- {({ handleLaunch }) => (
+ {({ handleLaunch, isLaunching }) => (
diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx
index f1e6775ed5..f9cc6961a8 100644
--- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx
+++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx
@@ -227,12 +227,13 @@ function WorkflowJobTemplateDetail({ template, i18n }) {
)}
{canLaunch && (
- {({ handleLaunch }) => (
+ {({ handleLaunch, isLaunching }) => (
diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerToolbar.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerToolbar.jsx
index c0c18e2ab7..0235bfd919 100644
--- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerToolbar.jsx
+++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerToolbar.jsx
@@ -125,11 +125,13 @@ function VisualizerToolbar({
resource={template}
aria-label={i18n._(t`Launch workflow`)}
>
- {({ handleLaunch }) => (
+ {({ handleLaunch, isLaunching }) => (