Fleet UI: Unreleased bug fixes for policy automations filtering (#41991)

This commit is contained in:
RachelElysia
2026-03-19 06:44:15 -04:00
committed by GitHub
parent 674b3c4d97
commit 9d913d766d
5 changed files with 201 additions and 102 deletions
@@ -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<string | null>(null);
// Maps frontend dropdown values to backend automation_type query param values
const AUTOMATION_FILTER_TO_API: Record<string, string> = {
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<CustomOptionType>) => {
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
? () => (
<DropdownWrapper
className={`${baseClass}__filter-automation-dropdown`}
name="filter-by-automation"
onChange={(val: SingleValue<CustomOptionType>) => {
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 (
<DropdownWrapper
className={`${baseClass}__filter-automation-dropdown`}
name="filter-by-automation"
value={getSelectedFilterOption()}
onChange={onChangeAutomationFilter}
placeholder="Filter by automation"
options={optionsForTeam}
variant="table-filter"
/>
);
}
: undefined;
const renderMainTable = () => {
@@ -1076,15 +1134,9 @@ const ManagePolicyPage = ({
if (globalPoliciesError) {
return <TableDataError verticalPaddingSize="pad-xxxlarge" />;
}
const filteredGlobalPolicies = filterGlobalPoliciesByAutomation(
globalPolicies || []
);
const filteredGlobalCount = automationFilter
? filteredGlobalPolicies.length
: globalPoliciesCount || 0;
return (
<PoliciesTable
policiesList={filteredGlobalPolicies}
policiesList={globalPolicies || []}
isLoading={isFetchingGlobalPolicies || isFetchingGlobalConfig}
onDeletePoliciesClick={onDeletePoliciesClick}
canAddOrDeletePolicies={canAddOrDeletePolicies}
@@ -1094,11 +1146,11 @@ const ManagePolicyPage = ({
isPremiumTier={isPremiumTier}
renderPoliciesCount={() =>
renderPoliciesCountAndLastUpdated(
filteredGlobalCount,
filteredGlobalPolicies
globalPoliciesCount,
globalPolicies
)
}
count={filteredGlobalCount}
count={globalPoliciesCount || 0}
searchQuery={searchQuery}
sortHeader={sortHeader}
sortDirection={sortDirection}
@@ -1115,11 +1167,7 @@ const ManagePolicyPage = ({
return <TableDataError verticalPaddingSize="pad-xxxlarge" />;
}
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 (
<div>
<PoliciesTable
@@ -1136,12 +1184,12 @@ const ManagePolicyPage = ({
currentAutomatedPolicies={currentAutomatedPolicies}
renderPoliciesCount={() =>
renderPoliciesCountAndLastUpdated(
displayedTeamCount,
teamPoliciesCountMergeInherited,
displayedTeamPolicies
)
}
isPremiumTier={isPremiumTier}
count={displayedTeamCount}
count={teamPoliciesCountMergeInherited || 0}
searchQuery={searchQuery}
sortHeader={sortHeader}
sortDirection={sortDirection}
@@ -20,7 +20,7 @@
}
&__filter-automation-dropdown {
min-width: 200px;
min-width: 277px;
}
&__manage-automations-wrapper {
@@ -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<
+19 -5
View File
@@ -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<IPoliciesApiParams, "query"> {
extends Pick<IGlobalPoliciesApiQueryParams, "query" | "automationType"> {
scope: "policiesCount";
}
@@ -68,7 +75,8 @@ export default {
orderKey = ORDER_KEY,
orderDirection: orderDir = ORDER_DIRECTION,
query,
}: IPoliciesApiParams): Promise<ILoadAllPoliciesResponse> => {
automationType,
}: IGlobalPoliciesApiQueryParams): Promise<ILoadAllPoliciesResponse> => {
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<IPoliciesApiParams, "query">): Promise<IPoliciesCountResponse> => {
automationType,
}: Pick<
IGlobalPoliciesApiQueryParams,
"query" | "automationType"
>): Promise<IPoliciesCountResponse> => {
const { GLOBAL_POLICIES } = endpoints;
const path = `${GLOBAL_POLICIES}/count`;
const queryParams = {
query,
automationType,
};
const snakeCaseParams = convertParamsToSnakeCase(queryParams);
const queryString = buildQueryStringFromParams(snakeCaseParams);
+17 -3
View File
@@ -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<IPoliciesApiParams, "query" | "teamId" | "mergeInherited"> {
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<IPoliciesCountResponse> => {
const { TEAM_POLICIES } = endpoints;
const path = `${TEAM_POLICIES(teamId)}/count`;
const queryParams = {
query,
mergeInherited,
automationType,
};
const snakeCaseParams = convertParamsToSnakeCase(queryParams);
const queryString = buildQueryStringFromParams(snakeCaseParams);