From 5657b2ddd170f9fb02585daf8b21447ed084fea1 Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:34:20 -0700 Subject: [PATCH] =?UTF-8?q?UI=20=E2=80=93=C2=A0when=20host's=20`scripts=5F?= =?UTF-8?q?enabled`=20is=20`null`,=20allow=20running=20scripts=20with=20wa?= =?UTF-8?q?rning=20(#18386)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Addresses #18382 - Do not disable option when `scripts_enabled: null` - Update disabled tooltip copy for 3 actions - Generalize dropdown with tooltip component to work for disabled and non-disabled (now unused, but was used in prototype before product input) cases ![Screenshot 2024-04-18 at 11 52 15 AM](https://github.com/fleetdm/fleet/assets/61553566/b1a08086-d39e-47ea-af8f-57f4e391e106) ![Screenshot 2024-04-18 at 12 29 18 PM](https://github.com/fleetdm/fleet/assets/61553566/25072573-ee79-45f4-951a-af072645a7d6) - [x] Added/updated tests - [x] Manual QA for all new/changed functionality - [x] Confirm previously disabled options with tooltips still work as expected with generalized dropdown option with tooltip --------- Co-authored-by: Jacob Shandling --- .../DisabledOptionTooltipWrapper/_styles.scss | 37 -------------- .../DisabledOptionTooltipWrapper/index.tsx | 1 - .../forms/fields/Dropdown/Dropdown.jsx | 10 ++-- .../DropdownOptionTooltipWrapper.tsx} | 10 ++-- .../DropdownOptionTooltipWrapper/_styles.scss | 37 ++++++++++++++ .../DropdownOptionTooltipWrapper/index.tsx | 1 + frontend/interfaces/dropdownOption.ts | 2 +- .../HostActionsDropdown.tests.tsx | 46 +++++++++++++++++ .../HostActionsDropdown.tsx | 2 - .../HostActionsDropdown/helpers.tsx | 49 ++++++------------- 10 files changed, 108 insertions(+), 87 deletions(-) delete mode 100644 frontend/components/forms/fields/Dropdown/DisabledOptionTooltipWrapper/_styles.scss delete mode 100644 frontend/components/forms/fields/Dropdown/DisabledOptionTooltipWrapper/index.tsx rename frontend/components/forms/fields/Dropdown/{DisabledOptionTooltipWrapper/DisabledOptionTooltipWrapper.tsx => DropdownOptionTooltipWrapper/DropdownOptionTooltipWrapper.tsx} (86%) create mode 100644 frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss create mode 100644 frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/index.tsx diff --git a/frontend/components/forms/fields/Dropdown/DisabledOptionTooltipWrapper/_styles.scss b/frontend/components/forms/fields/Dropdown/DisabledOptionTooltipWrapper/_styles.scss deleted file mode 100644 index a2f963ed66..0000000000 --- a/frontend/components/forms/fields/Dropdown/DisabledOptionTooltipWrapper/_styles.scss +++ /dev/null @@ -1,37 +0,0 @@ -.Select > .Select-menu-outer { - .is-disabled * { - color: $ui-fleet-black-50; - .disabled-option-tooltip-wrapper { - width: 100%; - } - .disabled-option-tooltip-wrapper__element { - // for broader tooltip activation area, equally increase padding and decrease margin - padding: 8px; - margin: -8px; - width: 100%; - } - .react-tooltip { - @include tooltip-text; - font-style: normal; - text-align: center; - } - - // arrow styles directly from react-tooltip-5 css - .tooltip-arrow { - width: 8px; - height: 8px; - } - [class*="react-tooltip__place-top"] > .styles-module_arrow__K0L3T { - transform: rotate(45deg); - } - [class*="react-tooltip__place-right"] > .styles-module_arrow__K0L3T { - transform: rotate(135deg); - } - [class*="react-tooltip__place-bottom"] > .styles-module_arrow__K0L3T { - transform: rotate(225deg); - } - [class*="react-tooltip__place-left"] > .styles-module_arrow__K0L3T { - transform: rotate(315deg); - } - } -} diff --git a/frontend/components/forms/fields/Dropdown/DisabledOptionTooltipWrapper/index.tsx b/frontend/components/forms/fields/Dropdown/DisabledOptionTooltipWrapper/index.tsx deleted file mode 100644 index 2a5818905b..0000000000 --- a/frontend/components/forms/fields/Dropdown/DisabledOptionTooltipWrapper/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./DisabledOptionTooltipWrapper"; diff --git a/frontend/components/forms/fields/Dropdown/Dropdown.jsx b/frontend/components/forms/fields/Dropdown/Dropdown.jsx index 0a9d657820..337851793c 100644 --- a/frontend/components/forms/fields/Dropdown/Dropdown.jsx +++ b/frontend/components/forms/fields/Dropdown/Dropdown.jsx @@ -7,7 +7,7 @@ import Select from "react-select"; import dropdownOptionInterface from "interfaces/dropdownOption"; import FormField from "components/forms/FormField"; import Icon from "components/Icon"; -import DisabledOptionTooltipWrapper from "./DisabledOptionTooltipWrapper"; +import DropdownOptionTooltipWrapper from "./DropdownOptionTooltipWrapper"; const baseClass = "dropdown"; @@ -110,11 +110,9 @@ class Dropdown extends Component { }; renderOption = (option) => { - if (option.disabledTooltipContent) { + if (option.tooltipContent) { return ( - +
{option.label} {option.helpText && ( @@ -123,7 +121,7 @@ class Dropdown extends Component { )}
-
+ ); } return ( diff --git a/frontend/components/forms/fields/Dropdown/DisabledOptionTooltipWrapper/DisabledOptionTooltipWrapper.tsx b/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/DropdownOptionTooltipWrapper.tsx similarity index 86% rename from frontend/components/forms/fields/Dropdown/DisabledOptionTooltipWrapper/DisabledOptionTooltipWrapper.tsx rename to frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/DropdownOptionTooltipWrapper.tsx index c6779a83cc..8fc4da9dc5 100644 --- a/frontend/components/forms/fields/Dropdown/DisabledOptionTooltipWrapper/DisabledOptionTooltipWrapper.tsx +++ b/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/DropdownOptionTooltipWrapper.tsx @@ -4,7 +4,7 @@ import { Tooltip as ReactTooltip5 } from "react-tooltip-5"; import { uniqueId } from "lodash"; -interface IDisabledOptionTooltipWrapper { +interface IDropdownOptionTooltipWrapper { children: React.ReactNode; isDelayed?: boolean; className?: string; @@ -16,9 +16,9 @@ interface IDisabledOptionTooltipWrapper { offset?: number; } -const baseClass = "disabled-option-tooltip-wrapper"; +const baseClass = "dropdown-option-tooltip-wrapper"; -const DisabledOptionTooltipWrapper = ({ +const DropdownOptionTooltipWrapper = ({ children, tipContent, isDelayed, @@ -27,7 +27,7 @@ const DisabledOptionTooltipWrapper = ({ clickable = true, place = "left", offset = 24, -}: IDisabledOptionTooltipWrapper) => { +}: IDropdownOptionTooltipWrapper) => { const wrapperClassNames = classnames(baseClass, className); const elementClassNames = classnames(`${baseClass}__element`); @@ -64,4 +64,4 @@ const DisabledOptionTooltipWrapper = ({ ); }; -export default DisabledOptionTooltipWrapper; +export default DropdownOptionTooltipWrapper; diff --git a/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss b/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss new file mode 100644 index 0000000000..c6f03b9cb7 --- /dev/null +++ b/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss @@ -0,0 +1,37 @@ +.Select > .Select-menu-outer { + .is-disabled * { + color: $ui-fleet-black-50; + } + .dropdown-option-tooltip-wrapper { + width: 100%; + } + .dropdown-option-tooltip-wrapper__element { + // for broader tooltip activation area, equally increase padding and decrease margin + padding: 8px; + margin: -8px; + width: 100%; + } + .react-tooltip { + @include tooltip-text; + font-style: normal; + text-align: center; + } + + // arrow styles directly from react-tooltip-5 css + .tooltip-arrow { + width: 8px; + height: 8px; + } + [class*="react-tooltip__place-top"] > .styles-module_arrow__K0L3T { + transform: rotate(45deg); + } + [class*="react-tooltip__place-right"] > .styles-module_arrow__K0L3T { + transform: rotate(135deg); + } + [class*="react-tooltip__place-bottom"] > .styles-module_arrow__K0L3T { + transform: rotate(225deg); + } + [class*="react-tooltip__place-left"] > .styles-module_arrow__K0L3T { + transform: rotate(315deg); + } +} diff --git a/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/index.tsx b/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/index.tsx new file mode 100644 index 0000000000..4d09cbeed5 --- /dev/null +++ b/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/index.tsx @@ -0,0 +1 @@ +export { default } from "./DropdownOptionTooltipWrapper"; diff --git a/frontend/interfaces/dropdownOption.ts b/frontend/interfaces/dropdownOption.ts index c592b4882c..a8895d48b9 100644 --- a/frontend/interfaces/dropdownOption.ts +++ b/frontend/interfaces/dropdownOption.ts @@ -11,5 +11,5 @@ export interface IDropdownOption { label: string | JSX.Element; value: string | number; premiumOnly?: boolean; - disabledTooltipContent?: string | JSX.Element; + tooltipContent?: string | JSX.Element; } diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx index 0c41f4fcda..ca03f1c8dc 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx @@ -1024,6 +1024,52 @@ describe("Host Actions Dropdown", () => { expect(screen.getByText("Run script")).toBeInTheDocument(); }); + it("renders the Run script action as enabled when `scripts_enabled` is `null`", async () => { + const render = createCustomRenderer({ + context: { + app: { + isGlobalAdmin: true, + currentUser: createMockUser(), + }, + }, + }); + + const { user } = render( + + ); + + await user.click(screen.getByText("Actions")); + + expect(screen.getByText("Run script")).toBeInTheDocument(); + + expect( + screen + .getByText("Run script") + .parentElement?.parentElement?.parentElement?.classList.contains( + "is-disabled" + ) + ).toBeFalsy(); + + await waitFor(() => { + waitFor(() => { + user.hover(screen.getByText("Run script")); + }); + + expect( + screen.queryByText(/fleetd agent with --enable-scripts/i) + ).toBeNull(); + }); + }); + it("renders the Run script action as disabled with a tooltip when scripts_enabled is set to false", async () => { const render = createCustomRenderer({ context: { diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx index b7fb56c05b..9d466d545d 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx @@ -42,7 +42,6 @@ const HostActionsDropdown = ({ isGlobalMaintainer = false, isMacMdmEnabledAndConfigured = false, isWindowsMdmEnabledAndConfigured = false, - isSandboxMode = false, currentUser, } = useContext(AppContext); @@ -73,7 +72,6 @@ const HostActionsDropdown = ({ isMacMdmEnabledAndConfigured, isWindowsMdmEnabledAndConfigured, doesStoreEncryptionKey: doesStoreEncryptionKey ?? false, - isSandboxMode, hostMdmDeviceStatus, hostScriptsEnabled, }); diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx index 2eb35cdf26..0a7a5f2842 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx @@ -5,8 +5,6 @@ import { IDropdownOption } from "interfaces/dropdownOption"; import { isLinuxLike } from "interfaces/platform"; import { isScriptSupportedPlatform } from "interfaces/script"; -import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip"; - import { HostMdmDeviceStatusUIState, isDeviceStatusUpdating, @@ -77,7 +75,6 @@ interface IHostActionConfigOptions { isMacMdmEnabledAndConfigured: boolean; isWindowsMdmEnabledAndConfigured: boolean; doesStoreEncryptionKey: boolean; - isSandboxMode: boolean; hostMdmDeviceStatus: HostMdmDeviceStatusUIState; hostScriptsEnabled: boolean | null; } @@ -239,7 +236,7 @@ const canRunScript = ({ ); }; -const filterOutOptions = ( +const removeUnavailableOptions = ( options: IDropdownOption[], config: IHostActionConfigOptions ) => { @@ -283,18 +280,17 @@ const filterOutOptions = ( return options; }; -const setOptionsAsDisabled = ( +const modifyOptions = ( options: IDropdownOption[], { isHostOnline, - isSandboxMode, hostMdmDeviceStatus, hostScriptsEnabled, hostPlatform, }: IHostActionConfigOptions ) => { // Available tooltips for disabled options - const disabledTooltipContent = (value: string | number) => { + const getDropdownOptionTooltipContent = (value: string | number) => { const tooltipAction: Record = { runScript: "run scripts on", wipe: "wipe", @@ -306,7 +302,9 @@ const setOptionsAsDisabled = ( <> To {tooltipAction[value]} this host, deploy the
- fleetd agent with --enable-scripts + fleetd agent with --enable-scripts and +
+ refetch host vitals ); } @@ -318,7 +316,7 @@ const setOptionsAsDisabled = ( const disableOptions = (optionsToDisable: IDropdownOption[]) => { optionsToDisable.forEach((option) => { option.disabled = true; - option.disabledTooltipContent = disabledTooltipContent(option.value); + option.tooltipContent = getDropdownOptionTooltipContent(option.value); }); }; @@ -336,7 +334,11 @@ const setOptionsAsDisabled = ( ); } - if (!hostScriptsEnabled) { + // null intentionally excluded from this condition: + // scripts_enabled === null means this agent is not an orbit agent, or this agent is version + // <=1.23.0 which is not collecting the scripts enabled info + // in each of these cases, we maintain these options + if (hostScriptsEnabled === false) { optionsToDisable = optionsToDisable.concat( options.filter((option) => option.value === "runScript") ); @@ -358,12 +360,6 @@ const setOptionsAsDisabled = ( ); } } - if (isSandboxMode) { - optionsToDisable = optionsToDisable.concat( - options.filter((option) => option.value === "transfer") - ); - } - disableOptions(optionsToDisable); return options; }; @@ -377,28 +373,11 @@ const setOptionsAsDisabled = ( export const generateHostActionOptions = (config: IHostActionConfigOptions) => { // deep clone to always start with a fresh copy of the default options. let options: IDropdownOption[] = cloneDeep([...DEFAULT_OPTIONS]); - options = filterOutOptions(options, config); + options = removeUnavailableOptions(options, config); if (options.length === 0) return options; - options = setOptionsAsDisabled(options, config); - - if (config.isSandboxMode) { - const premiumOnlyOptions: IDropdownOption[] = options.filter( - (option) => !!option.premiumOnly - ); - - premiumOnlyOptions.forEach((option) => { - option.label = ( - - {option.label} - - - ); - }); - } + options = modifyOptions(options, config); return options; };