mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 08:58:41 +00:00
Fleet UI: Disable host action buttons on click (noticeable on slow connections) (#36707)
This commit is contained in:
parent
6f632da279
commit
1922e772d7
4 changed files with 59 additions and 13 deletions
1
changes/36345-disable-action-buttons-onclick
Normal file
1
changes/36345-disable-action-buttons-onclick
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Fleet UI: Fixed software action buttons to disable immediately on click to prevent multiple clicks
|
||||
|
|
@ -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 = ({
|
|||
<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
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@
|
|||
justify-content: space-between;
|
||||
gap: $pad-medium;
|
||||
align-items: center;
|
||||
|
||||
.search-field__tooltip-container {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__table {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in a new issue