Skip to content

Policies: Include/Exclude label targeting on edit policy page (#33441) - #47354

Merged
nulmete merged 2 commits into
mainfrom
33441-policies-edit-custom-targets
Jun 11, 2026
Merged

Policies: Include/Exclude label targeting on edit policy page (#33441)#47354
nulmete merged 2 commits into
mainfrom
33441-policies-edit-custom-targets

Conversation

@nulmete

@nulmete nulmete commented Jun 10, 2026

Copy link
Copy Markdown
Member

Related issue: Resolves #46583

Figma: https://www.figma.com/design/0F1sw63SuYaKVWlcL7mnc6/-33441-Policies--Custom-targets-with-%22Include-any%22-and-%22Exclude-any%22?node-id=5319-2300&t=Fszpf83KhcZ7ViWh-0

Testing

  • Added/updated automated tests

  • QA'd all new/changed functionality manually

Note: selecting Exclude All will fail since BE doesn't support it yet. Will be tackled as part of #46582.

Screen.Recording.2026-06-10.at.3.57.56.PM.mov

Summary by CodeRabbit

  • New Features

    • Added support for excluding labels when targeting policy hosts, enabling more granular control over which hosts receive specific policy configurations.
  • Refactor

    • Streamlined label-targeting state management and UI components for improved performance and maintainability.

@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.89189% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.20%. Comparing base (b4c32d2) to head (244dd1b).

Files with missing lines Patch % Lines
frontend/context/policy.tsx 28.57% 4 Missing and 1 partial ⚠️
...policies/edit/components/PolicyForm/PolicyForm.tsx 80.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main   #47354    +/-   ##
========================================
  Coverage   67.19%   67.20%            
========================================
  Files        3274     3275     +1     
  Lines      227979   227976     -3     
  Branches    11751    11891   +140     
========================================
+ Hits       153200   153201     +1     
+ Misses      60963    60958     -5     
- Partials    13816    13817     +1     
Flag Coverage Δ
frontend 57.81% <91.89%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@nulmete
nulmete marked this pull request as ready for review June 10, 2026 19:00
@nulmete
nulmete requested a review from a team as a code owner June 10, 2026 19:00
@nulmete

nulmete commented Jun 10, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds "exclude all labels" support to the policies feature through a dedicated label-targeting hook. It consolidates the label-configuration type (ILabelConfig), extends the policy context with lastEditedQueryLabelsExcludeAll state, and introduces usePolicyLabelTargets hook to centralize label-selection state and payload construction. The PolicyForm and SaveNewPolicyModal are refactored to consume this hook instead of managing label state locally. The TargetLabelSelector component props are renamed from include/exclude to includeConfig/excludeConfig for clarity.

Possibly related PRs

  • fleetdm/fleet#47212: Introduces the TargetLabelSelector component that this PR builds upon by consolidating its type interface and prop names.
  • fleetdm/fleet#46444: Updates the configuration profiles label-targeting UI and payload construction in AddProfileModal, which this PR also updates to use the consolidated ILabelConfig type.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding include/exclude label targeting functionality to the edit policy page.
Linked Issues check ✅ Passed The PR implements frontend custom label targeting with include/exclude options and reuses the TargetLabelSelector modal as required by issue #46583.
Out of Scope Changes check ✅ Passed All changes directly support implementing custom label targeting functionality; no unrelated modifications were introduced.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed Pull request description includes related issue, testing checklist items, and implementation notes with video evidence.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 33441-policies-edit-custom-targets

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
frontend/pages/policies/hooks/usePolicyLabelTargets.tests.tsx (2)

49-163: ⚖️ Poor tradeoff

Consider expanding test coverage for completeness.

The test suite covers the main happy paths effectively, but several scenarios remain untested:

  • Mode switching: Toggling between any and all modes via includeConfig.onSelectMode / excludeConfig.onSelectMode
  • Deselecting labels: Setting value: false in onSelectLabel
  • Additional seeding cases: excludeAny and includeAll prop initialization
  • Error and loading states: Verifying isLoadingLabels and isErrorLabels propagation
  • Combined selections: Include + exclude labels selected simultaneously

These additions would improve confidence in edge cases, but the existing coverage is sufficient for the main flows.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/pages/policies/hooks/usePolicyLabelTargets.tests.tsx` around lines
49 - 163, Add unit tests for untested scenarios in usePolicyLabelTargets: cover
mode switching by calling selectorProps.includeConfig.onSelectMode and
selectorProps.excludeConfig.onSelectMode and asserting includeConfig.mode /
excludeConfig.mode and resulting getLabelsPayload; test deselecting labels by
invoking selectorProps.includeConfig.onSelectLabel / excludeConfig.onSelectLabel
with value: false and confirming payload updates; add seeding tests for
includeAll and excludeAny by initializing usePolicyLabelTargets with includeAll
and excludeAny props and asserting selectedTargetType, hasCustomLabels, and
getLabelsPayload; add tests that simulate loading and error label states by
asserting isLoadingLabels and isErrorLabels on selectorProps (mock the
labelSummariesHandler accordingly); and add a combined selection test that
selects both include and exclude labels and asserts the combined
getLabelsPayload.

82-89: ⚡ Quick win

Consider using waitFor instead of setTimeout for the non-fetch test.

The test uses setTimeout(resolve, 200) to verify that labels are not fetched when no team is selected. This approach is fragile in slow CI environments where 200ms might not be sufficient, and it adds unnecessary delay to the test suite.

♻️ Proposed fix using query state inspection

Instead of waiting an arbitrary duration, inspect the query state directly:

  it("does not fetch labels when no team is selected", async () => {
    const { result } = renderHook(() => usePolicyLabelTargets(), {
      wrapper: buildWrapper({ isPremiumTier: true, currentTeam: undefined }),
    });

-   await new Promise((resolve) => setTimeout(resolve, 200));
-   expect(result.current.selectorProps.labels).toEqual([]);
+   // Query should be disabled, so no loading state
+   expect(result.current.selectorProps.isLoadingLabels).toBe(false);
+   expect(result.current.selectorProps.labels).toEqual([]);
  });

This is faster and more deterministic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/pages/policies/hooks/usePolicyLabelTargets.tests.tsx` around lines
82 - 89, The test in usePolicyLabelTargets ("does not fetch labels when no team
is selected") uses a fixed setTimeout which is flaky; replace the arbitrary
delay with testing-library's waitFor to await the expected stable condition:
import waitFor from `@testing-library/react`, remove await new Promise(...), and
use await waitFor(() =>
expect(result.current.selectorProps.labels).toEqual([])); locate the test that
calls renderHook(() => usePolicyLabelTargets(), { wrapper: buildWrapper({
isPremiumTier: true, currentTeam: undefined }) }) and update it accordingly so
it deterministically waits for selectorProps.labels to be empty.
frontend/pages/policies/hooks/usePolicyLabelTargets.tsx (1)

155-167: ⚡ Quick win

Consider guarding against unstable prop references.

The useEffect reseeds all state whenever includeAny, includeAll, excludeAny, or excludeAll change identity. If a parent component passes new arrays with identical content on re-render, user selections will be lost. The test comments indicate this is addressed in production (PolicyContext provides stable references), but the hook doesn't enforce this contract.

Run the following script to verify all consumers pass stable references from PolicyContext:

#!/bin/bash
# Description: Verify that usePolicyLabelTargets consumers pass stable label props from PolicyContext

# Search for usages of the hook
ast-grep --pattern 'usePolicyLabelTargets($$$)'

If any consumer constructs label arrays inline (e.g., usePolicyLabelTargets({ includeAny: [...] })), the arrays won't be stable across renders.

🛡️ Optional deep-equality guard

If non-PolicyContext consumers are found, consider using a deep-equality check in the dependency array:

+ import { isEqual } from "lodash";
+
+ const [prevLabels, setPrevLabels] = useState({ includeAny, includeAll, excludeAny, excludeAll });
+
  useEffect(() => {
+   const current = { includeAny, includeAll, excludeAny, excludeAll };
+   if (isEqual(prevLabels, current)) return;
+   setPrevLabels(current);
+
    const seed = derivePolicyTargetState({
      includeAny,
      includeAll,
      excludeAny,
      excludeAll,
    });
    setSelectedTargetType(seed.targetType);
    setIncludeMode(seed.includeMode);
    setIncludeLabels(seed.includeLabels);
    setExcludeMode(seed.excludeMode);
    setExcludeLabels(seed.excludeLabels);
- }, [includeAny, includeAll, excludeAny, excludeAll]);
+ }, [includeAny, includeAll, excludeAny, excludeAll, prevLabels]);

However, this adds complexity and a lodash dependency, so only apply if instability is observed in practice.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/pages/policies/hooks/usePolicyLabelTargets.tsx` around lines 155 -
167, The effect in usePolicyLabelTargets reseeds state whenever the identity of
includeAny/includeAll/excludeAny/excludeAll changes, which will wipe user
selections if callers pass new-but-equal arrays; modify the effect to guard
against unstable prop references by comparing the previous and current label
arrays by deep equality before reseeding (i.e., compute a stable "seed" only
when the contents actually differ). Concretely, add a small equality check
around the derivePolicyTargetState call inside the existing useEffect in
usePolicyLabelTargets (or replace the effect with a deep-compare effect), and
only call setSelectedTargetType / setIncludeMode / setIncludeLabels /
setExcludeMode / setExcludeLabels when the deep-equality check shows the inputs
truly changed; you can implement the comparison with a lightweight helper (deep
equality or JSON.stringify) or use lodash.isEqual if available.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@frontend/pages/policies/hooks/usePolicyLabelTargets.tests.tsx`:
- Around line 49-163: Add unit tests for untested scenarios in
usePolicyLabelTargets: cover mode switching by calling
selectorProps.includeConfig.onSelectMode and
selectorProps.excludeConfig.onSelectMode and asserting includeConfig.mode /
excludeConfig.mode and resulting getLabelsPayload; test deselecting labels by
invoking selectorProps.includeConfig.onSelectLabel / excludeConfig.onSelectLabel
with value: false and confirming payload updates; add seeding tests for
includeAll and excludeAny by initializing usePolicyLabelTargets with includeAll
and excludeAny props and asserting selectedTargetType, hasCustomLabels, and
getLabelsPayload; add tests that simulate loading and error label states by
asserting isLoadingLabels and isErrorLabels on selectorProps (mock the
labelSummariesHandler accordingly); and add a combined selection test that
selects both include and exclude labels and asserts the combined
getLabelsPayload.
- Around line 82-89: The test in usePolicyLabelTargets ("does not fetch labels
when no team is selected") uses a fixed setTimeout which is flaky; replace the
arbitrary delay with testing-library's waitFor to await the expected stable
condition: import waitFor from `@testing-library/react`, remove await new
Promise(...), and use await waitFor(() =>
expect(result.current.selectorProps.labels).toEqual([])); locate the test that
calls renderHook(() => usePolicyLabelTargets(), { wrapper: buildWrapper({
isPremiumTier: true, currentTeam: undefined }) }) and update it accordingly so
it deterministically waits for selectorProps.labels to be empty.

In `@frontend/pages/policies/hooks/usePolicyLabelTargets.tsx`:
- Around line 155-167: The effect in usePolicyLabelTargets reseeds state
whenever the identity of includeAny/includeAll/excludeAny/excludeAll changes,
which will wipe user selections if callers pass new-but-equal arrays; modify the
effect to guard against unstable prop references by comparing the previous and
current label arrays by deep equality before reseeding (i.e., compute a stable
"seed" only when the contents actually differ). Concretely, add a small equality
check around the derivePolicyTargetState call inside the existing useEffect in
usePolicyLabelTargets (or replace the effect with a deep-compare effect), and
only call setSelectedTargetType / setIncludeMode / setIncludeLabels /
setExcludeMode / setExcludeLabels when the deep-equality check shows the inputs
truly changed; you can implement the comparison with a lightweight helper (deep
equality or JSON.stringify) or use lodash.isEqual if available.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 050fc649-8ea4-47e9-81c9-30ae8910cd39

📥 Commits

Reviewing files that changed from the base of the PR and between f308fec and 5eb8d4950ce3461770ac90ccfd002c3c6444194d.

📒 Files selected for processing (15)
  • frontend/components/TargetLabelSelector/TargetLabelSelector.tests.tsx
  • frontend/components/TargetLabelSelector/TargetLabelSelector.tsx
  • frontend/components/TargetLabelSelector/index.ts
  • frontend/context/policy.tsx
  • frontend/pages/ManageControlsPage/OSSettings/cards/ConfigurationProfiles/components/ProfileUploader/components/AddProfileModal/AddProfileModal.tsx
  • frontend/pages/policies/constants.ts
  • frontend/pages/policies/edit/EditPolicyPage.tsx
  • frontend/pages/policies/edit/components/PolicyForm/PolicyForm.tests.tsx
  • frontend/pages/policies/edit/components/PolicyForm/PolicyForm.tsx
  • frontend/pages/policies/edit/components/SaveNewPolicyModal/SaveNewPolicyModal.tests.tsx
  • frontend/pages/policies/edit/components/SaveNewPolicyModal/SaveNewPolicyModal.tsx
  • frontend/pages/policies/edit/screens/QueryEditor.tsx
  • frontend/pages/policies/hooks/index.ts
  • frontend/pages/policies/hooks/usePolicyLabelTargets.tests.tsx
  • frontend/pages/policies/hooks/usePolicyLabelTargets.tsx

getLabelsPayload: () => ReturnType<typeof buildPolicyLabelsPayload>;
}

const usePolicyLabelTargets = ({

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hook used both in Save and Edit forms, added for DRY purposes.

juan-fdz-hawa
juan-fdz-hawa previously approved these changes Jun 11, 2026
Base automatically changed from 33441-policies-save-modal-custom-targets to main June 11, 2026 13:00
@nulmete
nulmete dismissed juan-fdz-hawa’s stale review June 11, 2026 13:00

The base branch was changed.

nulmete added 2 commits June 11, 2026 10:04
Introduce usePolicyLabelTargets hook (owns label fetch, controlled state, and edit-init) and migrate PolicyForm + SaveNewPolicyModal onto the tabbed TargetLabelSelector. Plumb labels_exclude_all through PolicyContext, the edit-page load, and the create payload.
Add tests for mode switching, label deselection, include_all/exclude_any seeding, combined include+exclude payload, and loading/error states. Replace the flaky setTimeout in the no-team test with a deterministic labelsAPI.summary spy.
Copilot AI review requested due to automatic review settings June 11, 2026 13:05
@nulmete
nulmete force-pushed the 33441-policies-edit-custom-targets branch from 1250c38 to 244dd1b Compare June 11, 2026 13:05
@nulmete
nulmete requested a review from juan-fdz-hawa June 11, 2026 13:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Policies create/edit UI to support the newer tabbed include/exclude label targeting experience (and centralizes that logic in a reusable hook), while also refactoring TargetLabelSelector’s prop/types naming to match the new config shape.

Changes:

  • Added usePolicyLabelTargets hook to own label fetching, controlled selector state, and mapping UI selections to policy API payload fields.
  • Refactored policy create/edit flows to use the tabbed TargetLabelSelector and to plumb labels_exclude_all through form/context payloads.
  • Renamed TargetLabelSelector include/exclude config types/props (ILabelTabConfigILabelConfig, include/excludeincludeConfig/excludeConfig) and updated call sites + tests.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
frontend/pages/policies/hooks/usePolicyLabelTargets.tsx New hook: fetches label summaries, manages include/exclude state, builds policy labels payload.
frontend/pages/policies/hooks/usePolicyLabelTargets.tests.tsx Unit tests covering seeding, payload mapping, and label fetch states for the new hook.
frontend/pages/policies/hooks/index.ts Exposes usePolicyLabelTargets (default export + types) for policy pages.
frontend/pages/policies/edit/screens/QueryEditor.tsx Includes labels_exclude_all when constructing the create-policy payload.
frontend/pages/policies/edit/EditPolicyPage.tsx Stores labels_exclude_all from the loaded policy into PolicyContext.
frontend/pages/policies/edit/components/SaveNewPolicyModal/SaveNewPolicyModal.tsx Refactors modal to use usePolicyLabelTargets and new selector props; assigns labels payload via hook.
frontend/pages/policies/edit/components/SaveNewPolicyModal/SaveNewPolicyModal.tests.tsx Updates tests to align with hook-driven label fetching and new payload fields.
frontend/pages/policies/edit/components/PolicyForm/PolicyForm.tsx Refactors policy edit form to use usePolicyLabelTargets + tabbed selector; simplifies save-disabled logic.
frontend/pages/policies/edit/components/PolicyForm/PolicyForm.tests.tsx Updates/extends tests for new selector behavior (including exclude tab preload and combined include+exclude).
frontend/pages/policies/constants.ts Adds shared empty-state description constant for policy targeting selector.
frontend/pages/ManageControlsPage/OSSettings/.../AddProfileModal.tsx Updates selector prop/type names to the new includeConfig/excludeConfig + ILabelConfig.
frontend/context/policy.tsx Adds lastEditedQueryLabelsExcludeAll to context state and setter plumbing.
frontend/components/TargetLabelSelector/TargetLabelSelector.tsx Renames config types/props to ILabelConfig/includeConfig/excludeConfig and wires through the component.
frontend/components/TargetLabelSelector/TargetLabelSelector.tests.tsx Updates selector tests for renamed props/types.
frontend/components/TargetLabelSelector/index.ts Updates exported type name to ILabelConfig.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +196 to +221
const excludeConfig: ILabelConfig = {
selectedLabels: excludeLabels,
onSelectLabel: ({ name, value }) =>
setExcludeLabels((prev) => ({ ...prev, [name]: value })),
showModeToggle: true,
mode: excludeMode,
onSelectMode: setExcludeMode,
anyTooltip: (
<>
Will not target hosts that have{" "}
<em>
<b>any</b>
</em>{" "}
of these labels.
</>
),
allTooltip: (
<>
Will not target hosts that have{" "}
<em>
<b>all</b>
</em>{" "}
of these labels.
</>
),
};
@nulmete
nulmete merged commit 1a2b691 into main Jun 11, 2026
26 checks passed
@nulmete
nulmete deleted the 33441-policies-edit-custom-targets branch June 11, 2026 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Policies custom label targets] Frontend

3 participants