Skip to content

Commit

Permalink
Fleet UI: Disable install/uninstall actions if scripts are disabled (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
RachelElysia authored Sep 20, 2024
1 parent fc8b1d6 commit d7594d1
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,36 @@ const removeUnavailableOptions = (
return options;
};

// Available tooltips for disabled options
export const getDropdownOptionTooltipContent = (
value: string | number,
isHostOnline?: boolean
) => {
const tooltipAction: Record<string, string> = {
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
<br />
fleetd agent with --enable-scripts and
<br />
refetch host vitals
</>
);
}
if (!isHostOnline && value === "query") {
return <>You can&apos;t query an offline host.</>;
}
return undefined;
};

const modifyOptions = (
options: IDropdownOption[],
{
Expand All @@ -291,34 +321,13 @@ const modifyOptions = (
hostPlatform,
}: IHostActionConfigOptions
) => {
// Available tooltips for disabled options
const getDropdownOptionTooltipContent = (value: string | number) => {
const tooltipAction: Record<string, string> = {
runScript: "run scripts on",
wipe: "wipe",
lock: "lock",
unlock: "unlock",
};
if (tooltipAction[value]) {
return (
<>
To {tooltipAction[value]} this host, deploy the
<br />
fleetd agent with --enable-scripts and
<br />
refetch host vitals
</>
);
}
if (!isHostOnline && value === "query") {
return <>You can&apos;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
);
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
Expand Down
4 changes: 4 additions & 0 deletions frontend/pages/hosts/details/cards/Software/HostSoftware.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ interface IHostSoftwareProps {
hostTeamId: number;
onShowSoftwareDetails?: (software: IHostSoftware) => void;
isSoftwareEnabled?: boolean;
hostScriptsEnabled?: boolean;
isMyDevicePage?: boolean;
}

Expand Down Expand Up @@ -87,6 +88,7 @@ const HostSoftware = ({
platform,
softwareUpdatedAt,
hostCanWriteSoftware,
hostScriptsEnabled,
router,
queryParams,
pathname,
Expand Down Expand Up @@ -249,6 +251,7 @@ const HostSoftware = ({
router,
softwareIdActionPending,
userHasSWWritePermission,
hostScriptsEnabled,
onSelectAction,
teamId: hostTeamId,
hostCanWriteSoftware,
Expand All @@ -258,6 +261,7 @@ const HostSoftware = ({
router,
softwareIdActionPending,
userHasSWWritePermission,
hostScriptsEnabled,
onSelectAction,
hostTeamId,
hostCanWriteSoftware,
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -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);
Expand All @@ -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 || "")
) {
Expand All @@ -114,6 +130,7 @@ const generateActions = ({

interface ISoftwareTableHeadersProps {
userHasSWWritePermission: boolean;
hostScriptsEnabled?: boolean;
hostCanWriteSoftware: boolean;
softwareIdActionPending: number | null;
router: InjectedRouter;
Expand All @@ -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,
Expand Down Expand Up @@ -217,6 +235,7 @@ export const generateSoftwareTableHeaders = ({
placeholder="Actions"
options={generateActions({
userHasSWWritePermission,
hostScriptsEnabled,
hostCanWriteSoftware,
softwareIdActionPending,
softwareId,
Expand Down

0 comments on commit d7594d1

Please sign in to comment.