From dc0256441fcb157af5f0e529f0ec9ca343e4536e Mon Sep 17 00:00:00 2001 From: "Keith J. Grant" Date: Thu, 15 Apr 2021 14:51:39 -0700 Subject: [PATCH 1/3] disable launch buttons to prevent double-clicking --- awx/ui_next/src/components/JobList/JobListItem.jsx | 10 +++++++--- .../src/components/LaunchButton/LaunchButton.jsx | 7 +++++++ .../components/LaunchButton/ReLaunchDropDown.jsx | 14 +++++++++++--- .../components/TemplateList/TemplateListItem.jsx | 4 ++-- .../src/screens/Job/JobDetail/JobDetail.jsx | 6 ++++-- .../screens/Job/JobOutput/shared/OutputToolbar.jsx | 6 ++++-- .../ProjectJobTemplatesListItem.jsx | 3 ++- .../JobTemplateDetail/JobTemplateDetail.jsx | 3 ++- .../WorkflowJobTemplateDetail.jsx | 3 ++- .../VisualizerToolbar.jsx | 6 ++++-- 10 files changed, 45 insertions(+), 17 deletions(-) diff --git a/awx/ui_next/src/components/JobList/JobListItem.jsx b/awx/ui_next/src/components/JobList/JobListItem.jsx index 62b51a551d..0efc24829d 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, isSending }) => ( + )} ) : ( - {({ handleRelaunch }) => ( + {({ handleRelaunch, isSending }) => ( diff --git a/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx b/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx index e846c5fbd8..c6d0a72da4 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 [isSending, setIsSending] = useState(false); const [resourceCredentials, setResourceCredentials] = useState([]); const [error, setError] = useState(null); + const handleLaunch = async () => { + setIsSending(true); const readLaunch = resource.type === 'workflow_job_template' ? WorkflowJobTemplatesAPI.readLaunch(resource.id) @@ -96,6 +99,7 @@ function LaunchButton({ resource, i18n, children, history }) { history.push(`/jobs/${job.id}/output`); } catch (launchError) { setError(launchError); + setIsSending(false); } }; @@ -103,6 +107,7 @@ function LaunchButton({ resource, i18n, children, history }) { let readRelaunch; let relaunch; + setIsSending(true); if (resource.type === 'inventory_update') { // We'll need to handle the scenario where the src no longer exists readRelaunch = InventorySourcesAPI.readLaunchUpdate( @@ -146,6 +151,7 @@ function LaunchButton({ resource, i18n, children, history }) { } } catch (err) { setError(err); + setIsSending(false); } }; @@ -154,6 +160,7 @@ function LaunchButton({ resource, i18n, children, history }) { {children({ handleLaunch, handleRelaunch, + isSending, })} {error && ( { - setIsOPen(prev => !prev); + setIsOpen(prev => !prev); }; const dropdownItems = [ @@ -35,6 +41,7 @@ function ReLaunchDropDown({ isPrimary = false, handleRelaunch, i18n, ouiaId }) { onClick={() => { handleRelaunch({ hosts: 'all' }); }} + isDisabled={isSending} > {i18n._(t`All`)} , @@ -46,6 +53,7 @@ function ReLaunchDropDown({ isPrimary = false, handleRelaunch, i18n, ouiaId }) { onClick={() => { handleRelaunch({ hosts: 'failed' }); }} + isDisabled={isSending} > {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..cf8bb37202 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, isSending }) => ( 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..c13daeb498 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, isSending }) => ( )} ) : ( - {({ handleRelaunch }) => ( + {({ handleRelaunch, isSending }) => ( diff --git a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx index 9f93620f56..c6cdf5c791 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, isSending }) => ( diff --git a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx index a003dcc9d5..47c06a3f6d 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, isSending }) => ( diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx index f1e6775ed5..989ad776f1 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, isSending }) => ( diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerToolbar.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerToolbar.jsx index c0c18e2ab7..3083d7702f 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, isSending }) => ( From 5e228c4d988e94bf751c3fcc24086475dcc530ee Mon Sep 17 00:00:00 2001 From: "Keith J. Grant" Date: Mon, 19 Apr 2021 09:05:15 -0700 Subject: [PATCH 2/3] LaunchButton: rename isSending to isLaunching --- awx/ui_next/src/components/JobList/JobListItem.jsx | 8 ++++---- .../src/components/LaunchButton/LaunchButton.jsx | 12 ++++++------ .../src/components/LaunchButton/ReLaunchDropDown.jsx | 6 +++--- .../src/components/TemplateList/TemplateListItem.jsx | 4 ++-- awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx | 8 ++++---- .../screens/Job/JobOutput/shared/OutputToolbar.jsx | 8 ++++---- .../ProjectJobTemplatesListItem.jsx | 4 ++-- .../Template/JobTemplateDetail/JobTemplateDetail.jsx | 4 ++-- .../WorkflowJobTemplateDetail.jsx | 4 ++-- .../VisualizerToolbar.jsx | 4 ++-- 10 files changed, 31 insertions(+), 31 deletions(-) diff --git a/awx/ui_next/src/components/JobList/JobListItem.jsx b/awx/ui_next/src/components/JobList/JobListItem.jsx index 0efc24829d..0bd31dce77 100644 --- a/awx/ui_next/src/components/JobList/JobListItem.jsx +++ b/awx/ui_next/src/components/JobList/JobListItem.jsx @@ -91,22 +91,22 @@ function JobListItem({ > {job.status === 'failed' && job.type === 'job' ? ( - {({ handleRelaunch, isSending }) => ( + {({ handleRelaunch, isLaunching }) => ( )} ) : ( - {({ handleRelaunch, isSending }) => ( + {({ handleRelaunch, isLaunching }) => ( diff --git a/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx b/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx index c6d0a72da4..53f89b0063 100644 --- a/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx +++ b/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx @@ -36,12 +36,12 @@ function LaunchButton({ resource, i18n, children, history }) { const [showLaunchPrompt, setShowLaunchPrompt] = useState(false); const [launchConfig, setLaunchConfig] = useState(null); const [surveyConfig, setSurveyConfig] = useState(null); - const [isSending, setIsSending] = useState(false); + const [isLaunching, setIsLaunching] = useState(false); const [resourceCredentials, setResourceCredentials] = useState([]); const [error, setError] = useState(null); const handleLaunch = async () => { - setIsSending(true); + setIsLaunching(true); const readLaunch = resource.type === 'workflow_job_template' ? WorkflowJobTemplatesAPI.readLaunch(resource.id) @@ -99,7 +99,7 @@ function LaunchButton({ resource, i18n, children, history }) { history.push(`/jobs/${job.id}/output`); } catch (launchError) { setError(launchError); - setIsSending(false); + setIsLaunching(false); } }; @@ -107,7 +107,7 @@ function LaunchButton({ resource, i18n, children, history }) { let readRelaunch; let relaunch; - setIsSending(true); + setIsLaunching(true); if (resource.type === 'inventory_update') { // We'll need to handle the scenario where the src no longer exists readRelaunch = InventorySourcesAPI.readLaunchUpdate( @@ -151,7 +151,7 @@ function LaunchButton({ resource, i18n, children, history }) { } } catch (err) { setError(err); - setIsSending(false); + setIsLaunching(false); } }; @@ -160,7 +160,7 @@ function LaunchButton({ resource, i18n, children, history }) { {children({ handleLaunch, handleRelaunch, - isSending, + isLaunching, })} {error && ( { handleRelaunch({ hosts: 'all' }); }} - isDisabled={isSending} + isDisabled={isLaunching} > {i18n._(t`All`)} , @@ -53,7 +53,7 @@ function ReLaunchDropDown({ onClick={() => { handleRelaunch({ hosts: 'failed' }); }} - isDisabled={isSending} + 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 cf8bb37202..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, isSending }) => ( + {({ handleLaunch, 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 c13daeb498..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,23 +144,23 @@ const OutputToolbar = ({ > {job.status === 'failed' && job.type === 'job' ? ( - {({ handleRelaunch, isSending }) => ( + {({ handleRelaunch, isLaunching }) => ( )} ) : ( - {({ handleRelaunch, isSending }) => ( + {({ 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 c6cdf5c791..0f150e7bb7 100644 --- a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx +++ b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx @@ -116,13 +116,13 @@ function ProjectJobTemplateListItem({ {canLaunch && template.type === 'job_template' && ( - {({ handleLaunch, isSending }) => ( + {({ 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 47c06a3f6d..242092cc45 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateDetail/JobTemplateDetail.jsx @@ -387,13 +387,13 @@ function JobTemplateDetail({ i18n, template }) { )} {canLaunch && ( - {({ handleLaunch, isSending }) => ( + {({ 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 989ad776f1..f9cc6961a8 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateDetail/WorkflowJobTemplateDetail.jsx @@ -227,13 +227,13 @@ function WorkflowJobTemplateDetail({ template, i18n }) { )} {canLaunch && ( - {({ handleLaunch, isSending }) => ( + {({ 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 3083d7702f..0235bfd919 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerToolbar.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerToolbar.jsx @@ -125,12 +125,12 @@ function VisualizerToolbar({ resource={template} aria-label={i18n._(t`Launch workflow`)} > - {({ handleLaunch, isSending }) => ( + {({ handleLaunch, isLaunching }) => ( From dff43e973e39f1c59782c2cc35455e4bc69a5cfc Mon Sep 17 00:00:00 2001 From: "Keith J. Grant" Date: Mon, 19 Apr 2021 09:29:40 -0700 Subject: [PATCH 3/3] add LaunchButton test to cover disabled launch button behavior --- .../components/LaunchButton/LaunchButton.jsx | 2 + .../LaunchButton/LaunchButton.test.jsx | 43 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx b/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx index 53f89b0063..ab292ee976 100644 --- a/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx +++ b/awx/ui_next/src/components/LaunchButton/LaunchButton.jsx @@ -99,6 +99,7 @@ function LaunchButton({ resource, i18n, children, history }) { history.push(`/jobs/${job.id}/output`); } catch (launchError) { setError(launchError); + } finally { setIsLaunching(false); } }; @@ -151,6 +152,7 @@ function LaunchButton({ resource, i18n, children, history }) { } } catch (err) { setError(err); + } finally { setIsLaunching(false); } }; diff --git a/awx/ui_next/src/components/LaunchButton/LaunchButton.test.jsx b/awx/ui_next/src/components/LaunchButton/LaunchButton.test.jsx index 09af167081..9c2a3536f2 100644 --- a/awx/ui_next/src/components/LaunchButton/LaunchButton.test.jsx +++ b/awx/ui_next/src/components/LaunchButton/LaunchButton.test.jsx @@ -114,6 +114,49 @@ describe('LaunchButton', () => { 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 }) => ( +