Add Targeted platforms column and platform filter to Policies page (#44125)

- @noahtalerman: For the following quick win:
  - https://github.com/fleetdm/fleet/issues/23737

## Summary

Adds a "Targeted platforms" column and a platform filter dropdown to the
Policies page (`/policies/manage`), matching the pattern already used on
the Reports page (`/queries/manage`, `ManageQueriesPage`).

### Frontend
- New non-sortable **Targeted platforms** column rendered via
`PlatformCell`, sourced from each policy's comma-separated `platform`
field.
- New platform filter dropdown (All / macOS / Windows / Linux /
ChromeOS) wired as a `customControl` on the Policies table, alongside
the existing automation filter. Selecting a value pushes a new URL (not
a replace), resets `page` to 0, and updates the `platform` query param.
- `ManagePoliciesPage` reads `location.query.platform` and threads it
through to both `globalPoliciesAPI.loadAll` / `teamPoliciesAPI.loadAll`
and the react-query keys, plus the count endpoints. The
automation-filter and count "hide" conditions now include the platform
filter so they remain visible when only a platform filter is active.
- `frontend/services/entities/global_policies.ts` and `team_policies.ts`
accept an optional `platform` param (with `"all"` normalized to
`undefined`).
- Added tests for the new column and dropdown in
`PoliciesTable.tests.tsx`.

### Backend
- Added `Platform string ` + `` `query:"platform,optional"` `` to
`ListGlobalPoliciesRequest`, `CountGlobalPoliciesRequest`,
`ListTeamPoliciesRequest`, `CountTeamPoliciesRequest`.
- Extended datastore and service signatures (`ListGlobalPolicies`,
`ListTeamPolicies`, `ListMergedTeamPolicies`, `CountPolicies`,
`CountMergedTeamPolicies`, `ListGlobalPolicies`/`ListTeamPolicies` on
the service) to accept a `platform string` arg. Mocks and all call sites
updated.
- Platform filtering in SQL uses a new helper `platformFilterClause`:
  ```sql
  AND (p.platforms = '' OR FIND_IN_SET(?, p.platforms))
  ```
so policies targeting "all platforms" (empty `platforms` field) always
match regardless of the selected filter. `FIND_IN_SET` uses a bound
parameter (no injection risk).
- Added a new MySQL integration test `testPoliciesPlatformFilter`
covering empty-platform (match-all), per-platform filter, and
team/merged paths.

### Docs
- REST API docs for `GET /api/v1/fleet/global/policies`, `GET
/api/v1/fleet/fleets/:id/policies`, and the corresponding `/count`
endpoints now document the `platform` query param.
- Added `changes/policies-targeted-platforms-filter`.

## Behavior

- `platform=all` (or missing) returns all policies.
- Selecting a specific platform returns policies whose `platforms`
column is empty OR contains the selected token.
- The dropdown only renders when the table is searchable (results exist
OR any filter is active).
- Changing the filter pushes a new URL and resets the page.

# Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`.

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
- [x] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [x] Added/updated automated tests
- [ ] QA'd all new/changed functionality manually

Local verification:
- `go build ./...` — clean
- `go vet ./server/... ./cmd/... ./ee/...` — clean
- `make lint-go-incremental` — 0 issues
- Go service-level policy tests pass. MySQL integration tests compile
but could not be run locally (no Docker); CI will exercise the new
`testPoliciesPlatformFilter` test.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a "Targeted platforms" column with platform icons and an "All
platforms" option.
* Added a platform filter dropdown to scope policy lists; counts,
last-updated, and controls adapt when a platform filter is active.
Backend now honors an optional platform query parameter so filtering
returns matching policies.

* **Tests**
* Added and updated unit and integration tests covering the new column,
filter UI, and platform-filtered policy listings.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: nulmete <nicoulmete1@gmail.com>
This commit is contained in:
Allen Houchins
2026-07-10 13:32:16 -05:00
committed by GitHub
co-authored by Copilot copilot-swe-agent[bot] Cursor nulmete
parent 15118e4797
commit 078fbc0f40
29 changed files with 745 additions and 245 deletions
@@ -3970,7 +3970,7 @@ reports:
team, err := s.DS.TeamByName(ctx, teamName)
require.NoError(t, err)
pols, err := s.DS.ListMergedTeamPolicies(ctx, team.ID, fleet.ListOptions{}, "")
pols, err := s.DS.ListMergedTeamPolicies(ctx, team.ID, fleet.ListOptions{}, "", "")
require.NoError(t, err)
require.Len(t, pols, 3)
policyIDsByName := map[string]uint{}
@@ -3987,7 +3987,7 @@ reports:
"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile, "-f", teamFile,
}))
pols, err = s.DS.ListMergedTeamPolicies(ctx, team.ID, fleet.ListOptions{}, "")
pols, err = s.DS.ListMergedTeamPolicies(ctx, team.ID, fleet.ListOptions{}, "", "")
require.NoError(t, err)
require.Empty(t, pols, "all policies should be removed after FMA installer is removed")
@@ -4860,7 +4860,7 @@ settings:
installer, err := s.DS.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, nil, titles[0].ID, false)
require.NoError(t, err)
tmPols, err := s.DS.ListMergedTeamPolicies(ctx, 0, fleet.ListOptions{}, "")
tmPols, err := s.DS.ListMergedTeamPolicies(ctx, 0, fleet.ListOptions{}, "", "")
require.NoError(t, err)
require.Len(t, tmPols, 1)
require.Equal(t, "Install ruby", tmPols[0].Name)
@@ -4880,7 +4880,7 @@ settings:
installer, err = s.DS.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, &tm.ID, titles[0].ID, false)
require.NoError(t, err)
tmPols, err = s.DS.ListMergedTeamPolicies(ctx, tm.ID, fleet.ListOptions{}, "")
tmPols, err = s.DS.ListMergedTeamPolicies(ctx, tm.ID, fleet.ListOptions{}, "", "")
require.NoError(t, err)
require.Len(t, tmPols, 1)
require.Equal(t, "Install team ruby", tmPols[0].Name)
@@ -4934,7 +4934,7 @@ labels:
s.assertRealRunOutput(t, fleetctltest.RunAppForTest(t, []string{"gitops", "--config", fleetctlConfig.Name(), "-f", fullFile.Name()}))
// Verify policy, agent_options, controls, and reports were applied.
policies, err := s.DS.ListGlobalPolicies(ctx, fleet.ListOptions{})
policies, err := s.DS.ListGlobalPolicies(ctx, fleet.ListOptions{}, "")
require.NoError(t, err)
require.Len(t, policies, 1)
require.Equal(t, "Test Global Policy", policies[0].Name)
@@ -4977,7 +4977,7 @@ org_settings:
s.assertRealRunOutput(t, fleetctltest.RunAppForTest(t, []string{"gitops", "--config", fleetctlConfig.Name(), "-f", minimalFile.Name()}))
// Verify policies were cleared.
policies, err = s.DS.ListGlobalPolicies(ctx, fleet.ListOptions{})
policies, err = s.DS.ListGlobalPolicies(ctx, fleet.ListOptions{}, "")
require.NoError(t, err)
require.Len(t, policies, 0)
@@ -5082,7 +5082,7 @@ policies:
s.assertRealRunOutput(t, fleetctltest.RunAppForTest(t, []string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile, "-f", teamFile}))
// The global policy persisted both its include_any and exclude_all scopes.
globalPolicies, err := s.DS.ListGlobalPolicies(ctx, fleet.ListOptions{})
globalPolicies, err := s.DS.ListGlobalPolicies(ctx, fleet.ListOptions{}, "")
require.NoError(t, err)
require.Len(t, globalPolicies, 1)
gp := globalPolicies[0]
@@ -5097,7 +5097,7 @@ policies:
// The team policy persisted both its include_all and exclude_any scopes.
tm, err := s.DS.TeamByName(ctx, fleetName)
require.NoError(t, err)
teamPolicies, _, err := s.DS.ListTeamPolicies(ctx, tm.ID, fleet.ListOptions{}, fleet.ListOptions{}, "")
teamPolicies, _, err := s.DS.ListTeamPolicies(ctx, tm.ID, fleet.ListOptions{}, fleet.ListOptions{}, "", "")
require.NoError(t, err)
require.Len(t, teamPolicies, 1)
tp := teamPolicies[0]
@@ -5200,7 +5200,7 @@ labels:
fl, err := s.DS.TeamByName(ctx, fleetName)
require.NoError(t, err)
flPols, err := s.DS.ListMergedTeamPolicies(ctx, fl.ID, fleet.ListOptions{}, "")
flPols, err := s.DS.ListMergedTeamPolicies(ctx, fl.ID, fleet.ListOptions{}, "", "")
require.NoError(t, err)
require.Len(t, flPols, 1)
require.Equal(t, "Test Fleet Policy", flPols[0].Name)
@@ -5241,7 +5241,7 @@ name: %s
}))
// Verify policies were cleared.
flPols, err = s.DS.ListMergedTeamPolicies(ctx, fl.ID, fleet.ListOptions{}, "")
flPols, err = s.DS.ListMergedTeamPolicies(ctx, fl.ID, fleet.ListOptions{}, "", "")
require.NoError(t, err)
require.Len(t, flPols, 0)