UI – when host's scripts_enabled is null, allow running scripts with warning (#18386)

## 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 <jacob@fleetdm.com>
This commit is contained in:
Jacob Shandling
2024-04-18 14:34:20 -07:00
committed by GitHub
co-authored by Jacob Shandling
parent 39c35812c3
commit 5657b2ddd1
10 changed files with 108 additions and 87 deletions
@@ -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);
}
}
}
@@ -1 +0,0 @@
export { default } from "./DisabledOptionTooltipWrapper";
@@ -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 (
<DisabledOptionTooltipWrapper
tipContent={option.disabledTooltipContent}
>
<DropdownOptionTooltipWrapper tipContent={option.tooltipContent}>
<div className={`${baseClass}__option`}>
{option.label}
{option.helpText && (
@@ -123,7 +121,7 @@ class Dropdown extends Component {
</span>
)}
</div>
</DisabledOptionTooltipWrapper>
</DropdownOptionTooltipWrapper>
);
}
return (
@@ -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;
@@ -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);
}
}
@@ -0,0 +1 @@
export { default } from "./DropdownOptionTooltipWrapper";
+1 -1
View File
@@ -11,5 +11,5 @@ export interface IDropdownOption {
label: string | JSX.Element;
value: string | number;
premiumOnly?: boolean;
disabledTooltipContent?: string | JSX.Element;
tooltipContent?: string | JSX.Element;
}
@@ -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(
<HostActionsDropdown
hostTeamId={null}
onSelect={noop}
hostStatus="offline"
mdmName="Fleet"
hostPlatform="windows"
hostMdmEnrollmentStatus={null}
hostMdmDeviceStatus="unlocked"
hostScriptsEnabled={null}
/>
);
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: {
@@ -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,
});
@@ -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<string, string> = {
runScript: "run scripts on",
wipe: "wipe",
@@ -306,7 +302,9 @@ const setOptionsAsDisabled = (
<>
To {tooltipAction[value]} this host, deploy the
<br />
fleetd agent with --enable-scripts
fleetd agent with --enable-scripts and
<br />
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 = (
<span>
{option.label}
<PremiumFeatureIconWithTooltip
tooltipPositionOverrides={{ leftAdj: 2 }}
/>
</span>
);
});
}
options = modifyOptions(options, config);
return options;
};