Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/44624-policies-automations-filter-unassigned
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Fixed the Policies page automations filter disappearing from the UI when switching to the "Unassigned" fleet and selecting a different automation type.
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import React from "react";
import { screen, waitFor } from "@testing-library/react";
import { http, HttpResponse } from "msw";

import {
createCustomRenderer,
createMockRouter,
baseUrl,
} from "test/test-utils";
import mockServer from "test/mock-server";
import createMockConfig from "__mocks__/configMock";
import createMockUser from "__mocks__/userMock";
import createMockPolicy from "__mocks__/policyMock";

import ManagePoliciesPage from "./ManagePoliciesPage";

const FLEET_WITH_POLICIES_ID = 1;
const FLEET_WITHOUT_POLICIES_ID = 2;

const getConfigHandler = () =>
http.get(baseUrl("/config"), () => HttpResponse.json(createMockConfig()));

const getFleetHandler = () =>
http.get(baseUrl("/fleets/:id"), ({ params }) => {
const fleetId = Number(params.id);
const fleet = {
...createMockConfig(),
id: fleetId,
name: `Fleet ${fleetId}`,
};
return HttpResponse.json({ team: fleet, fleet });
});

const getFleetPoliciesHandler = () =>
http.get(baseUrl("/fleets/:fleetId/policies"), ({ params }) => {
const fleetId = Number(params.fleetId);
const policies =
fleetId === FLEET_WITH_POLICIES_ID
? [
createMockPolicy({
id: 1,
name: "Software policy",
team_id: fleetId,
install_software: { name: "Zoom", software_title_id: 1 },
}),
]
: [];
return HttpResponse.json({ policies });
});

const getFleetPoliciesCountHandler = () =>
http.get(baseUrl("/fleets/:fleetId/policies/count"), ({ params }) => {
const fleetId = Number(params.fleetId);
return HttpResponse.json({
count: fleetId === FLEET_WITH_POLICIES_ID ? 1 : 0,
});
});

const getGlobalPoliciesHandler = () =>
http.get(baseUrl("/policies"), () => HttpResponse.json({ policies: [] }));

const getGlobalPoliciesCountHandler = () =>
http.get(baseUrl("/policies/count"), () => HttpResponse.json({ count: 0 }));

const getAutomationFilterControl = (): HTMLElement => {
const control = document.querySelector(
".manage-policies-page__filter-automation-dropdown .react-select__control"
);
if (!control) {
throw new Error("Automations filter control not found");
}
return control as HTMLElement;
};

const setupHandlers = () => {
mockServer.use(
getConfigHandler(),
getFleetHandler(),
getFleetPoliciesHandler(),
getFleetPoliciesCountHandler(),
getGlobalPoliciesHandler(),
getGlobalPoliciesCountHandler()
);
};

describe("ManagePoliciesPage - automations filter", () => {
const renderPage = (fleetId: string, automationType?: string) => {
setupHandlers();

const render = createCustomRenderer({
withBackendMock: true,
context: {
app: {
currentUser: createMockUser({ global_role: "admin" }),
isGlobalAdmin: true,
isOnGlobalTeam: true,
isPremiumTier: true,
availableTeams: [
{ id: -1, name: "All fleets" },
{ id: 0, name: "Unassigned" },
{ id: FLEET_WITH_POLICIES_ID, name: "Fleet with policies" },
{ id: FLEET_WITHOUT_POLICIES_ID, name: "Fleet without policies" },
],
config: createMockConfig(),
setCurrentTeam: jest.fn(),
setFilteredPoliciesPath: jest.fn(),
setConfig: jest.fn(),
},
},
});

const query: Record<string, string> = { fleet_id: fleetId };
if (automationType) {
query.automation_type = automationType;
}

return render(
<ManagePoliciesPage
router={createMockRouter()}
location={{
action: "PUSH",
hash: "",
key: "",
pathname: "/policies/manage",
query,
search: `?${new URLSearchParams(query).toString()}`,
}}
/>
);
};

it("keeps the automations filter dropdown visible and enabled for a fleet with no policies when a filter is present in the URL", async () => {
renderPage(FLEET_WITHOUT_POLICIES_ID.toString(), "software");

await waitFor(() => {
expect(
screen.getByText("No policies match the current filters.")
).toBeInTheDocument();
});

// Per the fix for #44624: when a filter is present in the query params,
// the automations filter dropdown must remain visible and enabled (and
// reflect the selected filter) even though this fleet has zero policies.
expect(screen.getByText("Software")).toBeInTheDocument();
expect(getAutomationFilterControl()).not.toHaveClass(
"react-select__control--is-disabled"
);
});

it("offers software/scripts/conditional access automation types for the 'Unassigned' fleet, but not calendar", async () => {
const { user } = renderPage("0", "software");

await waitFor(() => {
expect(
screen.getByText("No policies match the current filters.")
).toBeInTheDocument();
});

// The selected filter persists (sticky), matching what's in the URL.
expect(screen.getByText("Software")).toBeInTheDocument();

// "Unassigned" is a real, policy-bearing fleet (unlike "All fleets", which
// is restricted to webhook/ticket-only automations) -- it should offer
// every automation type EXCEPT calendar events, which
// PolicyAutomationsFields hardcodes as fleet-only (never available for
// "All fleets" or "Unassigned").
await user.click(getAutomationFilterControl());

expect(screen.getByText("Scripts")).toBeInTheDocument();
expect(screen.getByText("Conditional access")).toBeInTheDocument();
expect(screen.queryByText("Calendar")).not.toBeInTheDocument();
});

it("rejects an automation_type=calendar query param for the 'Unassigned' fleet, since calendar isn't a valid option there", async () => {
renderPage("0", "calendar");

await waitFor(() => {
expect(
screen.getByText("No policies for this fleet")
).toBeInTheDocument();
});

// "calendar" isn't a valid filter for "Unassigned", so it must not stick
// as the selected value -- the filter should fall back to its default.
expect(screen.getByText("All automations")).toBeInTheDocument();
expect(screen.queryByText("Calendar")).not.toBeInTheDocument();
});
});
36 changes: 26 additions & 10 deletions frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ import {
} from "interfaces/policy";
import {
API_ALL_TEAMS_ID,
API_NO_TEAM_ID,
APP_CONTEXT_ALL_TEAMS_ID,
ITeamConfig,
} from "interfaces/team";
import { isQueryablePlatform } from "interfaces/platform";

Expand Down Expand Up @@ -105,6 +105,22 @@ const AUTOMATION_TYPES: AutomationType[] = [

const GLOBAL_AUTOMATION_TYPES: GlobalPoliciesAutomationType[] = ["other"];

const getValidAutomationTypesForTeam = (
teamIdForApi: number | undefined
): (AutomationType | GlobalPoliciesAutomationType)[] => {
if (teamIdForApi === undefined) {
// All fleets → global policies only support webhook/ticket automations.
return GLOBAL_AUTOMATION_TYPES;
}
if (teamIdForApi === API_NO_TEAM_ID) {
// Unassigned supports every automation type EXCEPT calendar events,
// which PolicyAutomationsFields hardcodes as fleet-only (never available
// for "All fleets" or "Unassigned").
return AUTOMATION_TYPES.filter((type) => type !== "calendar");
}
return AUTOMATION_TYPES;
};

const baseClass = "manage-policies-page";

const ManagePolicyPage = ({
Expand Down Expand Up @@ -191,9 +207,7 @@ const ManagePolicyPage = ({
return null;
}

const validValues = isAllTeamsSelected
? GLOBAL_AUTOMATION_TYPES
: AUTOMATION_TYPES;
const validValues = getValidAutomationTypesForTeam(teamIdForApi);

return (validValues as string[]).includes(automationQueryParam)
? automationQueryParam
Expand Down Expand Up @@ -782,12 +796,14 @@ const ManagePolicyPage = ({
!automationFilter &&
!targetedPlatformParam;

// No team ID = All fleets → only show "all" and "other" options
const optionsForTeam = teamIdForApi
? automationFilterOptions
: automationFilterOptions.filter((opt) =>
["all", "other"].includes(opt.value as string)
);
const validAutomationTypesForTeam = getValidAutomationTypesForTeam(
teamIdForApi
);
const optionsForTeam = automationFilterOptions.filter(
(opt) =>
opt.value === "all" ||
(validAutomationTypesForTeam as string[]).includes(opt.value)
);

return (
<DropdownWrapper
Expand Down
Loading