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   - [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:
co-authored by
Jacob Shandling
parent
39c35812c3
commit
5657b2ddd1
@@ -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 (
|
||||
|
||||
+5
-5
@@ -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";
|
||||
@@ -11,5 +11,5 @@ export interface IDropdownOption {
|
||||
label: string | JSX.Element;
|
||||
value: string | number;
|
||||
premiumOnly?: boolean;
|
||||
disabledTooltipContent?: string | JSX.Element;
|
||||
tooltipContent?: string | JSX.Element;
|
||||
}
|
||||
|
||||
+46
@@ -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: {
|
||||
|
||||
-2
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user