Fleet UI: Disable host action buttons on click (noticeable on slow connections) (#36707)

This commit is contained in:
RachelElysia 2025-12-05 07:04:58 -05:00 committed by GitHub
parent 6f632da279
commit 1922e772d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 59 additions and 13 deletions

View file

@ -0,0 +1 @@
* Fleet UI: Fixed software action buttons to disable immediately on click to prevent multiple clicks

View file

@ -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 roundtrip 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 = ({
<HostInstallerActionButton
baseClass={baseClass}
tooltip={installTooltip}
disabled={installDisabled}
onClick={() =>
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 = ({
<HostInstallerActionButton
baseClass={baseClass}
tooltip={uninstallTooltip}
disabled={uninstallDisabled}
onClick={onClickUninstallAction}
disabled={uninstallDisabled || isInstallUninstallPendingLocal}
onClick={handleUninstallClick}
icon={buttonDisplayConfig.uninstall.icon}
text={buttonDisplayConfig.uninstall.text}
testId={`${baseClass}__uninstall-button--test`}
@ -350,7 +369,7 @@ export const HostInstallerActionCell = ({
options={getMoreActionsDropdownOptions(
canViewOpenInstructions,
canUninstallSoftware,
uninstallDisabled,
uninstallDisabled || isInstallUninstallPendingLocal,
uninstallTooltip,
buttonDisplayConfig.uninstall.text
)}

View file

@ -21,6 +21,10 @@
justify-content: space-between;
gap: $pad-medium;
align-items: center;
.search-field__tooltip-container {
width: 100%;
}
}
&__table {

View file

@ -1,4 +1,4 @@
import React from "react";
import React, { useState, useEffect } from "react";
import {
IDeviceSoftwareWithUiStatus,
IHostSoftwareUiStatus,
@ -68,13 +68,31 @@ const TileActionStatus = ({
software,
onActionClick,
}: TileActionStatusProps) => {
// 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 = ({
</div>
)}
{actionLabel && (
<Button variant="inverse" onClick={() => onActionClick(software)}>
<Button
variant="inverse"
onClick={handleClick}
disabled={disableAction}
>
{actionLabel}
</Button>
)}