diff --git a/changes/36345-disable-action-buttons-onclick b/changes/36345-disable-action-buttons-onclick new file mode 100644 index 0000000000..2c605bcdd7 --- /dev/null +++ b/changes/36345-disable-action-buttons-onclick @@ -0,0 +1 @@ +* Fleet UI: Fixed software action buttons to disable immediately on click to prevent multiple clicks \ No newline at end of file diff --git a/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostInstallerActionCell/HostInstallerActionCell.tsx b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostInstallerActionCell/HostInstallerActionCell.tsx index 7633a5de36..0a5c9e8bce 100644 --- a/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostInstallerActionCell/HostInstallerActionCell.tsx +++ b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostInstallerActionCell/HostInstallerActionCell.tsx @@ -233,7 +233,18 @@ export const HostInstallerActionCell = ({ uninstall: getInstallerActionButtonConfig("uninstall", ui_status), }); + // Local “clicked” state so the button disables immediately + const [ + isInstallUninstallPendingLocal, + setIsInstallUninstallPendingLocal, + ] = useState(false); + useEffect(() => { + // reset local pending state when status leaves pending (API round‑trip finished) + if (status !== "pending_install" && status !== "pending_uninstall") { + setIsInstallUninstallPendingLocal(false); + } + // We update the text/icon only when we see a change to a non-pending status // Pending statuses keep the original text shown (e.g. "Retry" text on failed // install shouldn't change to "Install" text because it was clicked and went @@ -291,10 +302,23 @@ export const HostInstallerActionCell = ({ installedVersionsDetected, }); + // Wrap handlers to disable action button(s) immediately, important for slow connections/APIs + const handleInstallClick = () => { + if (installDisabled || isInstallUninstallPendingLocal) return; + setIsInstallUninstallPendingLocal(true); + onClickInstallAction(id, SCRIPT_PACKAGE_SOURCES.includes(software.source)); + }; + + const handleUninstallClick = () => { + if (uninstallDisabled || isInstallUninstallPendingLocal) return; + setIsInstallUninstallPendingLocal(true); + onClickUninstallAction(); + }; + const onSelectOption = (option: string) => { switch (option) { case "uninstall": - onClickUninstallAction(); + handleUninstallClick(); break; case "instructions": onClickOpenInstructionsAction && onClickOpenInstructionsAction(); @@ -307,13 +331,8 @@ export const HostInstallerActionCell = ({ - onClickInstallAction( - id, - SCRIPT_PACKAGE_SOURCES.includes(software.source) - ) - } + disabled={installDisabled || isInstallUninstallPendingLocal} + onClick={handleInstallClick} icon={buttonDisplayConfig.install.icon} text={buttonDisplayConfig.install.text} testId={`${baseClass}__install-button--test`} @@ -329,8 +348,8 @@ export const HostInstallerActionCell = ({ { + // Local “clicked” state so the button disables immediately + const [disableAction, setDisableAction] = useState(false); + const actionLabel = getTileActionLabel(software.ui_status); const isError = isSoftwareErrorStatus(software.ui_status); + useEffect(() => { + if ( + !isSoftwareInProgressStatus(software.ui_status) && + !isSoftwarePendingStatus(software.ui_status) + ) { + setDisableAction(false); + } + }, [software.ui_status]); + const isActiveAction = isSoftwareInProgressStatus(software.ui_status) || isSoftwarePendingStatus(software.ui_status); + // Wrap handler to disable action button immediately, important for slow connections/APIs + const handleClick = () => { + setDisableAction(true); + onActionClick(software); + }; + const renderActiveActionStatus = () => { return ( <> @@ -94,7 +112,11 @@ const TileActionStatus = ({ )} {actionLabel && ( - )}