From d7594d1f1d156bbe7d4046f2e3afba670dc66583 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:19:43 -0700 Subject: [PATCH] Fleet UI: Disable install/uninstall actions if scripts are disabled (#22240) --- .../HostActionsDropdown/helpers.tsx | 59 +++++++------ .../HostDetailsPage/HostDetailsPage.tsx | 1 + .../details/cards/Software/HostSoftware.tsx | 4 + .../HostSoftwareTableConfig.tests.tsx | 83 +++++++++++++++++++ .../Software/HostSoftwareTableConfig.tsx | 49 +++++++---- 5 files changed, 156 insertions(+), 40 deletions(-) create mode 100644 frontend/pages/hosts/details/cards/Software/HostSoftwareTableConfig.tests.tsx diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx index 3665687d0353..5590846c9e49 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx @@ -282,6 +282,36 @@ const removeUnavailableOptions = ( return options; }; +// Available tooltips for disabled options +export const getDropdownOptionTooltipContent = ( + value: string | number, + isHostOnline?: boolean +) => { + const tooltipAction: Record = { + runScript: "run scripts on", + wipe: "wipe", + lock: "lock", + unlock: "unlock", + installSoftware: "install software on", // Host software dropdown option + uninstallSoftware: "uninstall software on", // Host software dropdown option + }; + if (tooltipAction[value]) { + return ( + <> + To {tooltipAction[value]} this host, deploy the +
+ fleetd agent with --enable-scripts and +
+ refetch host vitals + + ); + } + if (!isHostOnline && value === "query") { + return <>You can't query an offline host.; + } + return undefined; +}; + const modifyOptions = ( options: IDropdownOption[], { @@ -291,34 +321,13 @@ const modifyOptions = ( hostPlatform, }: IHostActionConfigOptions ) => { - // Available tooltips for disabled options - const getDropdownOptionTooltipContent = (value: string | number) => { - const tooltipAction: Record = { - runScript: "run scripts on", - wipe: "wipe", - lock: "lock", - unlock: "unlock", - }; - if (tooltipAction[value]) { - return ( - <> - To {tooltipAction[value]} this host, deploy the -
- fleetd agent with --enable-scripts and -
- refetch host vitals - - ); - } - if (!isHostOnline && value === "query") { - return <>You can't query an offline host.; - } - }; - const disableOptions = (optionsToDisable: IDropdownOption[]) => { optionsToDisable.forEach((option) => { option.disabled = true; - option.tooltipContent = getDropdownOptionTooltipContent(option.value); + option.tooltipContent = getDropdownOptionTooltipContent( + option.value, + isHostOnline + ); }); }; diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 5c627edb9a62..0c56d7c6ca4d 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -946,6 +946,7 @@ const HostDetailsPage = ({ platform={host.platform} softwareUpdatedAt={host.software_updated_at} hostCanWriteSoftware={!!host.orbit_version || isIosOrIpadosHost} + hostScriptsEnabled={host.scripts_enabled || false} isSoftwareEnabled={featuresConfig?.enable_software_inventory} router={router} queryParams={parseHostSoftwareQueryParams(location.query)} diff --git a/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx b/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx index 818fbcd7d989..1853d0aeee34 100644 --- a/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx +++ b/frontend/pages/hosts/details/cards/Software/HostSoftware.tsx @@ -44,6 +44,7 @@ interface IHostSoftwareProps { hostTeamId: number; onShowSoftwareDetails?: (software: IHostSoftware) => void; isSoftwareEnabled?: boolean; + hostScriptsEnabled?: boolean; isMyDevicePage?: boolean; } @@ -87,6 +88,7 @@ const HostSoftware = ({ platform, softwareUpdatedAt, hostCanWriteSoftware, + hostScriptsEnabled, router, queryParams, pathname, @@ -249,6 +251,7 @@ const HostSoftware = ({ router, softwareIdActionPending, userHasSWWritePermission, + hostScriptsEnabled, onSelectAction, teamId: hostTeamId, hostCanWriteSoftware, @@ -258,6 +261,7 @@ const HostSoftware = ({ router, softwareIdActionPending, userHasSWWritePermission, + hostScriptsEnabled, onSelectAction, hostTeamId, hostCanWriteSoftware, diff --git a/frontend/pages/hosts/details/cards/Software/HostSoftwareTableConfig.tests.tsx b/frontend/pages/hosts/details/cards/Software/HostSoftwareTableConfig.tests.tsx new file mode 100644 index 000000000000..aa14172c86f8 --- /dev/null +++ b/frontend/pages/hosts/details/cards/Software/HostSoftwareTableConfig.tests.tsx @@ -0,0 +1,83 @@ +import { + generateActions, + DEFAULT_ACTION_OPTIONS, + generateActionsProps, +} from "./HostSoftwareTableConfig"; + +describe("generateActions", () => { + const defaultProps: generateActionsProps = { + userHasSWWritePermission: true, + hostScriptsEnabled: true, + hostCanWriteSoftware: true, + softwareIdActionPending: null, + softwareId: 1, + status: null, + software_package: null, + app_store_app: null, + }; + + it("returns default actions when user has write permission and scripts are enabled", () => { + const actions = generateActions(defaultProps); + expect(actions).toEqual(DEFAULT_ACTION_OPTIONS); + }); + + it("removes install and uninstall actions when user has no write permission", () => { + const props = { ...defaultProps, userHasSWWritePermission: false }; + const actions = generateActions(props); + expect(actions.find((a) => a.value === "install")).toBeUndefined(); + expect(actions.find((a) => a.value === "uninstall")).toBeUndefined(); + }); + + it("disables install and uninstall actions when host scripts are disabled", () => { + const props = { ...defaultProps, hostScriptsEnabled: false }; + const actions = generateActions(props); + expect(actions.find((a) => a.value === "install")?.disabled).toBe(true); + expect(actions.find((a) => a.value === "uninstall")?.disabled).toBe(true); + }); + + it("disables install and uninstall actions when locally pending (waiting for API response)", () => { + const props = { + ...defaultProps, + softwareIdActionPending: 1, + softwareId: 1, + }; + const actions = generateActions(props); + expect(actions.find((a) => a.value === "install")?.disabled).toBe(true); + expect(actions.find((a) => a.value === "uninstall")?.disabled).toBe(true); + }); + + it("disables install and uninstall actions when pending install status", () => { + const props: generateActionsProps = { + ...defaultProps, + status: "pending_install", + }; + const actions = generateActions(props); + expect(actions.find((a) => a.value === "install")?.disabled).toBe(true); + expect(actions.find((a) => a.value === "uninstall")?.disabled).toBe(true); + }); + + it("disables install and uninstall actions when pending uninstall status", () => { + const props: generateActionsProps = { + ...defaultProps, + status: "pending_uninstall", + }; + const actions = generateActions(props); + expect(actions.find((a) => a.value === "install")?.disabled).toBe(true); + expect(actions.find((a) => a.value === "uninstall")?.disabled).toBe(true); + }); + + it("removes uninstall action for VPP apps", () => { + const props: generateActionsProps = { + ...defaultProps, + app_store_app: { + app_store_id: "1", + self_service: false, + icon_url: "", + version: "", + last_install: { command_uuid: "", installed_at: "" }, + }, + }; + const actions = generateActions(props); + expect(actions.find((a) => a.value === "uninstall")).toBeUndefined(); + }); +}); diff --git a/frontend/pages/hosts/details/cards/Software/HostSoftwareTableConfig.tsx b/frontend/pages/hosts/details/cards/Software/HostSoftwareTableConfig.tsx index dd4aef833e74..53256f50bffb 100644 --- a/frontend/pages/hosts/details/cards/Software/HostSoftwareTableConfig.tsx +++ b/frontend/pages/hosts/details/cards/Software/HostSoftwareTableConfig.tsx @@ -29,8 +29,9 @@ import VersionCell from "pages/SoftwarePage/components/VersionCell"; import { getVulnerabilities } from "pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTitlesTableConfig"; import InstallStatusCell from "./InstallStatusCell"; +import { getDropdownOptionTooltipContent } from "../../HostDetailsPage/HostActionsDropdown/helpers"; -const DEFAULT_ACTION_OPTIONS: IDropdownOption[] = [ +export const DEFAULT_ACTION_OPTIONS: IDropdownOption[] = [ { value: "showDetails", label: "Show details", disabled: false }, { value: "install", label: "Install", disabled: false }, { value: "uninstall", label: "Uninstall", disabled: false }, @@ -50,24 +51,25 @@ type IInstalledVersionsCellProps = CellProps< >; type IVulnerabilitiesCellProps = IInstalledVersionsCellProps; -const generateActions = ({ - userHasSWWritePermission, - // Commenting below in case there is a quick decision to use these conditions after all - // hostCanWriteSoftware, - // software_package, - softwareIdActionPending, - softwareId, - status, - app_store_app, -}: { +export interface generateActionsProps { userHasSWWritePermission: boolean; + hostScriptsEnabled: boolean; hostCanWriteSoftware: boolean; softwareIdActionPending: number | null; softwareId: number; status: SoftwareInstallStatus | null; software_package: IHostSoftwarePackage | null; app_store_app: IHostAppStoreApp | null; -}) => { +} + +export const generateActions = ({ + userHasSWWritePermission, + hostScriptsEnabled, + softwareIdActionPending, + softwareId, + status, + app_store_app, +}: generateActionsProps) => { // this gives us a clean slate of the default actions so we can modify // the options. const actions = cloneDeep(DEFAULT_ACTION_OPTIONS); @@ -88,15 +90,29 @@ const generateActions = ({ } if (!userHasSWWritePermission) { - actions.splice(indexInstallAction, 1); + // Reverse order to not change index of subsequent array element before removal actions.splice(indexUninstallAction, 1); + actions.splice(indexInstallAction, 1); } else { + // if host's scripts are disabled, disable install/uninstall with tooltip + if (!hostScriptsEnabled) { + actions[indexInstallAction].disabled = true; + actions[indexUninstallAction].disabled = true; + + actions[ + indexInstallAction + ].tooltipContent = getDropdownOptionTooltipContent("installSoftware"); + actions[ + indexUninstallAction + ].tooltipContent = getDropdownOptionTooltipContent("uninstallSoftware"); + } + // user has software write permission for host const pendingStatuses = ["pending_install", "pending_uninstall"]; + // if locally pending (waiting for API response) or pending install/uninstall, + // disable both install and uninstall if ( - // if locally pending (waiting for API response) or pending install/uninstall, disable both - // install and uninstall softwareId === softwareIdActionPending || pendingStatuses.includes(status || "") ) { @@ -114,6 +130,7 @@ const generateActions = ({ interface ISoftwareTableHeadersProps { userHasSWWritePermission: boolean; + hostScriptsEnabled?: boolean; hostCanWriteSoftware: boolean; softwareIdActionPending: number | null; router: InjectedRouter; @@ -125,6 +142,7 @@ interface ISoftwareTableHeadersProps { // more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties export const generateSoftwareTableHeaders = ({ userHasSWWritePermission, + hostScriptsEnabled = false, hostCanWriteSoftware, softwareIdActionPending, router, @@ -217,6 +235,7 @@ export const generateSoftwareTableHeaders = ({ placeholder="Actions" options={generateActions({ userHasSWWritePermission, + hostScriptsEnabled, hostCanWriteSoftware, softwareIdActionPending, softwareId,