From 9d913d766dfca9bd80ca64d8fb97bb06f535b721 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Thu, 19 Mar 2026 06:44:15 -0400 Subject: [PATCH] Fleet UI: Unreleased bug fixes for policy automations filtering (#41991) --- .../ManagePoliciesPage/ManagePoliciesPage.tsx | 214 +++++++++++------- .../policies/ManagePoliciesPage/_styles.scss | 2 +- .../PoliciesPaginatedList.tsx | 43 +++- frontend/services/entities/global_policies.ts | 24 +- frontend/services/entities/team_policies.ts | 20 +- 5 files changed, 201 insertions(+), 102 deletions(-) diff --git a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx index 13c37ca400..2f4b1e3c4c 100644 --- a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx @@ -34,12 +34,14 @@ import { TooltipContent } from "interfaces/dropdownOption"; import configAPI from "services/entities/config"; import globalPoliciesAPI, { + GlobalPoliciesAutomationType, IPoliciesCountQueryKey, IPoliciesQueryKey, } from "services/entities/global_policies"; import teamPoliciesAPI, { ITeamPoliciesCountQueryKey, ITeamPoliciesQueryKey, + AutomationType, } from "services/entities/team_policies"; import teamsAPI, { ILoadTeamResponse } from "services/entities/teams"; @@ -88,6 +90,7 @@ interface IManagePoliciesPageProps { order_key?: string; order_direction?: "asc" | "desc"; page?: string; + automation_type?: AutomationType; }; search: string; }; @@ -104,6 +107,16 @@ const [ "Could not update policy automations.", ]; +const AUTOMATION_TYPES: AutomationType[] = [ + "software", + "scripts", + "calendar", + "conditional_access", + "other", +]; + +const GLOBAL_AUTOMATION_TYPES: GlobalPoliciesAutomationType[] = ["other"]; + const baseClass = "manage-policies-page"; const ManagePolicyPage = ({ @@ -194,6 +207,21 @@ const ManagePolicyPage = ({ DEFAULT_SORT_DIRECTION)(); const page = queryParams && queryParams.page ? parseInt(queryParams?.page, 10) : 0; + const initialAutomationFilter = (() => { + const automationQueryParam = queryParams.automation_type; + + if (!automationQueryParam) { + return null; + } + + const validValues = isAllTeamsSelected + ? GLOBAL_AUTOMATION_TYPES + : AUTOMATION_TYPES; + + return (validValues as string[]).includes(automationQueryParam) + ? automationQueryParam + : null; + })(); // Needs update on location change or table state might not match URL const [searchQuery, setSearchQuery] = useState(initialSearchQuery); @@ -205,16 +233,9 @@ const ManagePolicyPage = ({ const [sortDirection, setSortDirection] = useState< "asc" | "desc" | undefined >(initialSortDirection); - const [automationFilter, setAutomationFilter] = useState(null); - - // Maps frontend dropdown values to backend automation_type query param values - const AUTOMATION_FILTER_TO_API: Record = { - install_software: "software", - run_script: "scripts", - calendar_events: "calendar", - conditional_access: "conditional_access", - other_workflows: "other", - }; + const [automationFilter, setAutomationFilter] = useState< + AutomationType | GlobalPoliciesAutomationType | null + >(initialAutomationFilter); useEffect(() => { setLastEditedQueryPlatform(null); @@ -227,12 +248,14 @@ const ManagePolicyPage = ({ setSearchQuery(initialSearchQuery); setSortHeader(initialSortHeader); setSortDirection(initialSortDirection); + setAutomationFilter(initialAutomationFilter); }, [ location, isRouteOk, initialSearchQuery, initialSortHeader, initialSortDirection, + initialAutomationFilter, ]); useEffect(() => { @@ -271,6 +294,7 @@ const ManagePolicyPage = ({ query: searchQuery, orderDirection: sortDirection, orderKey: sortHeader, + automationType: automationFilter as GlobalPoliciesAutomationType, }, ], ({ queryKey }) => { @@ -292,6 +316,7 @@ const ManagePolicyPage = ({ { scope: "policiesCount", query: !isAllTeamsSelected ? "" : searchQuery, + automationType: automationFilter as GlobalPoliciesAutomationType, }, ], ({ queryKey }) => globalPoliciesAPI.getCount(queryKey[0]), @@ -327,9 +352,7 @@ const ManagePolicyPage = ({ teamId: teamIdForApi || 0, // no teams does inherit mergeInherited: true, - automationType: automationFilter - ? AUTOMATION_FILTER_TO_API[automationFilter] - : undefined, + automationType: automationFilter as AutomationType, }, ], ({ queryKey }) => { @@ -357,6 +380,7 @@ const ManagePolicyPage = ({ query: searchQuery, teamId: teamIdForApi || 0, // TODO: Fix number/undefined type mergeInherited: true, + automationType: automationFilter as AutomationType, }, ], ({ queryKey }) => teamPoliciesAPI.getCount(queryKey[0]), @@ -929,6 +953,21 @@ const ManagePolicyPage = ({ toggleDeletePoliciesModal, ]); + const onChangeAutomationFilter = (val: SingleValue) => { + const automationType = val?.value; + + const locationPath = getNextLocationPath({ + pathPrefix: PATHS.MANAGE_POLICIES, + queryParams: { + ...queryParams, + page: "0", + automation_type: automationType === "all" ? undefined : automationType, + }, + }); + + router?.push(locationPath); + }; + const policiesErrors = !isAllTeamsSelected ? teamPoliciesError : globalPoliciesError; @@ -974,7 +1013,7 @@ const ManagePolicyPage = ({ count?: number, policies?: IPolicyStats[] ) => { - // Hide count if fetching count || there are errors OR there are no policy results with no a search filter + // Hide count if fetching count || there are errors OR there are no policy results with no filters (search or automation dropdown) const isFetchingCount = !isAllTeamsSelected ? isFetchingTeamCountMergeInherited : isFetchingGlobalCount; @@ -982,7 +1021,7 @@ const ManagePolicyPage = ({ const hide = isFetchingCount || policiesErrors || - (!policyResults && searchQuery === ""); + (!policyResults && searchQuery === "" && !automationFilter); if (hide) { return null; @@ -1009,61 +1048,80 @@ const ManagePolicyPage = ({ ); }; - // Client-side filtering is still needed for global policies since the - // global policies endpoint does not support automation_type. Team policies - // use the server-side automation_type query param instead. - const filterGlobalPoliciesByAutomation = ( - policies: IPolicyStats[] - ): IPolicyStats[] => { - if (!automationFilter) return policies; - return policies.filter((p) => { - switch (automationFilter) { - case "install_software": - return !!p.install_software; - case "run_script": - return !!p.run_script; - case "calendar_events": - return p.calendar_events_enabled; - case "conditional_access": - return p.conditional_access_enabled; - case "other_workflows": - return currentAutomatedPolicies.includes(p.id); - default: - return true; - } - }); - }; - const automationFilterOptions: CustomOptionType[] = [ - { label: "All automations", value: "all" }, - { label: "Software", value: "install_software" }, - { label: "Scripts", value: "run_script" }, - { label: "Calendar", value: "calendar_events" }, - { label: "Conditional access", value: "conditional_access" }, - { label: "Other", value: "other_workflows" }, + { + label: "All policies", + value: "all", + helpText: "All policies added to Fleet.", + }, + { + label: "Software", + value: "software", + helpText: "Policies with software automation enabled.", + }, + { + label: "Scripts", + value: "scripts", + helpText: "Policies with script automation enabled.", + }, + { + label: "Calendar", + value: "calendar", + helpText: "Policies with calendar event automation enabled.", + }, + { + label: "Conditional access", + value: "conditional_access", + helpText: "Policies with conditional access automation enabled.", + }, + { + label: "Other", + value: "other", + helpText: "Policies with other automation enabled.", + }, ]; + const allPoliciesOption = automationFilterOptions[0]; // value: "all" + + const getSelectedFilterOption = () => { + if (!automationFilter) { + return allPoliciesOption; // Default to all policies option + } + return automationFilterOptions.find( + (opt) => opt.value === automationFilter + ); + }; + const renderAutomationFilter = isPremiumTier - ? () => ( - ) => { - const newFilter = - val?.value && val.value !== "all" ? val.value : null; - setAutomationFilter(newFilter); - // Reset to first page when filter changes - const locationPath = getNextLocationPath({ - pathPrefix: PATHS.MANAGE_POLICIES, - queryParams: { ...queryParams, page: "0" }, - }); - router?.push(locationPath); - }} - placeholder="Filter by automation" - options={automationFilterOptions} - variant="table-filter" - /> - ) + ? () => { + // Hide dropdown if there are errors OR there are no policy results with no filters (search or automation dropdown) + const hide = + policiesErrors || + (!policyResults && searchQuery === "" && !automationFilter); + + if (hide) { + return null; + } + + // 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) + ); + + return ( + + ); + } : undefined; const renderMainTable = () => { @@ -1076,15 +1134,9 @@ const ManagePolicyPage = ({ if (globalPoliciesError) { return ; } - const filteredGlobalPolicies = filterGlobalPoliciesByAutomation( - globalPolicies || [] - ); - const filteredGlobalCount = automationFilter - ? filteredGlobalPolicies.length - : globalPoliciesCount || 0; return ( renderPoliciesCountAndLastUpdated( - filteredGlobalCount, - filteredGlobalPolicies + globalPoliciesCount, + globalPolicies ) } - count={filteredGlobalCount} + count={globalPoliciesCount || 0} searchQuery={searchQuery} sortHeader={sortHeader} sortDirection={sortDirection} @@ -1115,11 +1167,7 @@ const ManagePolicyPage = ({ return ; } const displayedTeamPolicies = teamPolicies || []; - // When a filter is active, use the returned array length as the count - // since the count endpoint doesn't support automation_type yet. - const displayedTeamCount = automationFilter - ? displayedTeamPolicies.length - : teamPoliciesCountMergeInherited || 0; + return (
renderPoliciesCountAndLastUpdated( - displayedTeamCount, + teamPoliciesCountMergeInherited, displayedTeamPolicies ) } isPremiumTier={isPremiumTier} - count={displayedTeamCount} + count={teamPoliciesCountMergeInherited || 0} searchQuery={searchQuery} sortHeader={sortHeader} sortDirection={sortDirection} diff --git a/frontend/pages/policies/ManagePoliciesPage/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/_styles.scss index 576b9290d1..3c34b40e15 100644 --- a/frontend/pages/policies/ManagePoliciesPage/_styles.scss +++ b/frontend/pages/policies/ManagePoliciesPage/_styles.scss @@ -20,7 +20,7 @@ } &__filter-automation-dropdown { - min-width: 200px; + min-width: 277px; } &__manage-automations-wrapper { diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesPaginatedList/PoliciesPaginatedList.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesPaginatedList/PoliciesPaginatedList.tsx index 8313f67c5e..c6f6500358 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesPaginatedList/PoliciesPaginatedList.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesPaginatedList/PoliciesPaginatedList.tsx @@ -21,7 +21,9 @@ import teamPoliciesAPI, { IPoliciesApiParams, IPoliciesCountApiParams, } from "services/entities/team_policies"; -import globalPoliciesAPI from "services/entities/global_policies"; +import globalPoliciesAPI, { + IGlobalPoliciesApiQueryParams, +} from "services/entities/global_policies"; import { APP_CONTEXT_ALL_TEAMS_ID } from "interfaces/team"; import { QueryablePlatform, isQueryablePlatform } from "interfaces/platform"; @@ -164,10 +166,12 @@ function PoliciesPaginatedList( orderDirection: "asc" as const, orderKey: DEFAULT_SORT_COLUMN, teamId, + automationType: undefined, }; countQueryKey = { query: "", teamId, + automationType: undefined, }; } else { policiesQueryKey = { @@ -178,11 +182,13 @@ function PoliciesPaginatedList( orderKey: DEFAULT_SORT_COLUMN, teamId, mergeInherited: false, + automationType: undefined, }; countQueryKey = { query: "", teamId, mergeInherited: false, + automationType: undefined, }; } @@ -206,11 +212,18 @@ function PoliciesPaginatedList( ILoadAllPoliciesResponse, Error, IFormPolicy[] - >([policiesQueryKey], () => globalPoliciesAPI.loadAllNew(policiesQueryKey), { - enabled: teamId === APP_CONTEXT_ALL_TEAMS_ID, - keepPreviousData: true, - select: marshallApiResponse, - }); + >( + [policiesQueryKey], + () => + globalPoliciesAPI.loadAllNew( + policiesQueryKey as IGlobalPoliciesApiQueryParams + ), + { + enabled: teamId === APP_CONTEXT_ALL_TEAMS_ID, + keepPreviousData: true, + select: marshallApiResponse, + } + ); // Team policies query const { data: teamData, isFetching: teamIsLoading } = useQuery< @@ -232,10 +245,20 @@ function PoliciesPaginatedList( IPoliciesCountResponse, Error, number - >([countQueryKey], () => globalPoliciesAPI.getCount(countQueryKey), { - enabled: teamId === APP_CONTEXT_ALL_TEAMS_ID, - select: (countResponse: IPoliciesCountResponse) => countResponse.count, - }); + >( + [countQueryKey], + () => + globalPoliciesAPI.getCount( + countQueryKey as Pick< + IGlobalPoliciesApiQueryParams, + "query" | "automationType" + > + ), + { + enabled: teamId === APP_CONTEXT_ALL_TEAMS_ID, + select: (countResponse: IPoliciesCountResponse) => countResponse.count, + } + ); // Team count query const { data: teamCount, isFetching: teamIsFetchingCount } = useQuery< diff --git a/frontend/services/entities/global_policies.ts b/frontend/services/entities/global_policies.ts index 0cf44c0ca4..25e414b436 100644 --- a/frontend/services/entities/global_policies.ts +++ b/frontend/services/entities/global_policies.ts @@ -11,21 +11,28 @@ import { buildQueryStringFromParams, convertParamsToSnakeCase, } from "utilities/url"; +import { AutomationType } from "./team_policies"; -interface IPoliciesApiParams { +export type GlobalPoliciesAutomationType = Exclude< + AutomationType, + "software" | "scripts" | "conditional_access" | "calendar" +>; + +export interface IGlobalPoliciesApiQueryParams { page?: number; perPage?: number; orderKey?: string; orderDirection?: "asc" | "desc"; query?: string; + automationType?: GlobalPoliciesAutomationType; } -export interface IPoliciesQueryKey extends IPoliciesApiParams { +export interface IPoliciesQueryKey extends IGlobalPoliciesApiQueryParams { scope: "globalPolicies"; } export interface IPoliciesCountQueryKey - extends Pick { + extends Pick { scope: "policiesCount"; } @@ -68,7 +75,8 @@ export default { orderKey = ORDER_KEY, orderDirection: orderDir = ORDER_DIRECTION, query, - }: IPoliciesApiParams): Promise => { + automationType, + }: IGlobalPoliciesApiQueryParams): Promise => { const { GLOBAL_POLICIES } = endpoints; const queryParams = { @@ -77,6 +85,7 @@ export default { orderKey, orderDirection: orderDir, query, + automationType, }; const snakeCaseParams = convertParamsToSnakeCase(queryParams); @@ -87,11 +96,16 @@ export default { }, getCount: ({ query, - }: Pick): Promise => { + automationType, + }: Pick< + IGlobalPoliciesApiQueryParams, + "query" | "automationType" + >): Promise => { const { GLOBAL_POLICIES } = endpoints; const path = `${GLOBAL_POLICIES}/count`; const queryParams = { query, + automationType, }; const snakeCaseParams = convertParamsToSnakeCase(queryParams); const queryString = buildQueryStringFromParams(snakeCaseParams); diff --git a/frontend/services/entities/team_policies.ts b/frontend/services/entities/team_policies.ts index 6661cf2f23..39614a5ae7 100644 --- a/frontend/services/entities/team_policies.ts +++ b/frontend/services/entities/team_policies.ts @@ -11,6 +11,14 @@ import { } from "interfaces/policy"; import { API_NO_TEAM_ID } from "interfaces/team"; import { buildQueryStringFromParams, QueryParams } from "utilities/url"; +import { GlobalPoliciesAutomationType } from "./global_policies"; + +export type AutomationType = + | "software" + | "scripts" + | "calendar" + | "conditional_access" + | "other"; interface IPoliciesApiQueryParams { page?: number; @@ -18,7 +26,7 @@ interface IPoliciesApiQueryParams { orderKey?: string; orderDirection?: "asc" | "desc"; query?: string; - automationType?: string; + automationType?: AutomationType | GlobalPoliciesAutomationType; } export interface IPoliciesApiParams extends IPoliciesApiQueryParams { @@ -31,7 +39,10 @@ export interface ITeamPoliciesQueryKey extends IPoliciesApiParams { } export interface ITeamPoliciesCountQueryKey - extends Pick { + extends Pick< + IPoliciesApiParams, + "query" | "teamId" | "mergeInherited" | "automationType" + > { scope: "teamPoliciesCountMergeInherited" | "teamPoliciesCount"; } @@ -39,6 +50,7 @@ export interface IPoliciesCountApiParams { teamId: number; query?: string; mergeInherited?: boolean; + automationType?: AutomationType; } const ORDER_KEY = "name"; @@ -179,15 +191,17 @@ export default { query, teamId, mergeInherited = true, + automationType, }: Pick< IPoliciesCountApiParams, - "query" | "teamId" | "mergeInherited" + "query" | "teamId" | "mergeInherited" | "automationType" >): Promise => { const { TEAM_POLICIES } = endpoints; const path = `${TEAM_POLICIES(teamId)}/count`; const queryParams = { query, mergeInherited, + automationType, }; const snakeCaseParams = convertParamsToSnakeCase(queryParams); const queryString = buildQueryStringFromParams(snakeCaseParams);