Skip to content

feat(studio): Anonymizer entity picker categories and custom labels [ASTD-335] - #912

Merged
marcusds merged 3 commits into
mainfrom
astd-335-anonymizer-entity-picker/mschwab
Jul 28, 2026
Merged

feat(studio): Anonymizer entity picker categories and custom labels [ASTD-335]#912
marcusds merged 3 commits into
mainfrom
astd-335-anonymizer-entity-picker/mschwab

Conversation

@marcusds

@marcusds marcusds commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Closes ASTD-335.

Depends on #936 (ASTD-344) — merge that first. It fixes ControlledCombobox so a multi-select can read typed input; this branch is stacked on it and the entity picker now uses the shared component instead of a hand-rolled KUI Combobox. Until #936 lands, the diff here includes its commit.

Upgrades the lean entity picker from ASTD-327 to the full design.

What's here

Before Now
Flat list of 65 labels Grouped under 13 categories, in design order
Default labels only Type any label; offered as a Custom label item
Plain multi-select Chips below the field, coloured by category
Include-defaults hid the picker Both together; request merges defaults + picks

Notable decisions

The category map is curated in Studio. /entity-labels returns a flat array of 65 bare strings with no category field (verified against a live service), so grouping has to live client-side. The map is keyed to the exact label ids the API returns and covers all 65. Anything the API adds later that isn't mapped lands in an Other section rather than vanishing from the menu — buildEntitySections is driven by what the API returns, not by the map.

Custom labels are a synthetic menu item. KUI's Combobox has no creatable/free-solo mode, and the shared ControlledCombobox isn't usable here: it pins inputValue to '' in multi-select, and its freeForm path writes the raw input string into what is a string[] field (ControlledCombobox/index.tsx:75-82 — a latent bug for any multi+freeForm caller, left alone here to keep this PR off shared components). So this builds directly on KUI Combobox + useController, and surfaces whatever is typed as a one-item Custom label section.

Trigger uses multipleMode="count". Rendering chips below the field while the combobox also renders its own tags in the trigger duplicated every selection. renderValue doesn't suppress them — it only applies when closed, and returning null reads as "no custom renderer". count gives a compact "2 selected" summary and leaves the coloured chips as the single source of truth.

Include-defaults merge. entity_labels replaces the default set server-side, so "include defaults" has to transmit them explicitly. buildDetectConfig unions defaults with the picks, and omits detect entirely when the selection adds nothing (empty, or a subset of the defaults) so the server stays on its own defaults instead of receiving a redundant 65-item list.

No Strict/Permissive tabs. The ticket flagged these as "consider". Pulling the Figma Entities frame shows Strict/Permissive are the variant names of the design component, whose two states render the existing Custom / Auto-detect segmented control — already shipped in ASTD-327. Nothing to build; the descriptive copy was updated to match the design's wording.

Testing

  • 30 unit tests (9 new): category coverage and uniqueness, section grouping order, Other fallback, custom-candidate rules, and the four detect-merge cases
  • typecheck, eslint, prettier clean
  • Driven in a running Studio against a live platform: category headings render, a typed ice_cream_flavor is offered and selectable, chips appear coloured with working removal, and the checkbox shows the live count of 65

Screenshots

01-category-menu 02-custom-label-candidate 03-chips-and-defaults-merge

Summary by CodeRabbit

  • New Features

    • Improved anonymizer entity selection with categorized labels and color-coded tags.
    • Added support for entering and selecting custom entity labels.
    • Default entity labels are now loaded automatically for the current workspace.
    • Added validation to require default entities or at least one custom label.
    • Full Run is disabled while entity labels are loading.
  • Bug Fixes

    • Improved job configuration to correctly combine default and custom entity selections.

@marcusds
marcusds requested review from a team as code owners July 27, 2026 17:29
@github-actions github-actions Bot added the feat label Jul 27, 2026
Base automatically changed from astd-334-anonymizer-rewrite-params/mschwab to main July 27, 2026 21:24
@marcusds
marcusds force-pushed the astd-335-anonymizer-entity-picker/mschwab branch from 3f7ee39 to 23fd3e8 Compare July 27, 2026 22:05
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The anonymizer builder now categorizes entity labels, supports custom labels and removable selections, loads workspace defaults, validates custom-mode selections, and builds detection configuration from default and user-selected labels.

Changes

Anonymizer entity labels

Layer / File(s) Summary
Entity label categorization
web/packages/studio/src/routes/AnonymizerBuilderRoute/constants.ts, web/packages/studio/src/routes/AnonymizerBuilderRoute/entityItems.ts, web/packages/studio/src/routes/AnonymizerBuilderRoute/entityItems.test.ts
Curated categories, tag colors, “Other” grouping, custom-label candidates, and corresponding tests were added.
Builder selection and submission
web/packages/studio/src/routes/AnonymizerBuilderRoute/components/EntitiesSection.tsx, web/packages/studio/src/routes/AnonymizerBuilderRoute/components/AnonymizerBuilderForm.tsx, web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.ts, web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.test.ts
Custom-mode selection uses categorized combobox items and removable tags; workspace defaults are passed into job-request construction; custom selections are validated and translated into config.detect.

Sequence Diagram(s)

sequenceDiagram
  participant AnonymizerBuilderForm
  participant useAnonymizerListEntityLabels
  participant buildAnonymizerJobRequest
  participant useAnonymizerCreateRunJob
  AnonymizerBuilderForm->>useAnonymizerListEntityLabels: load workspace default labels
  useAnonymizerListEntityLabels-->>AnonymizerBuilderForm: return defaultEntityLabels
  AnonymizerBuilderForm->>buildAnonymizerJobRequest: build request from form and defaults
  buildAnonymizerJobRequest-->>AnonymizerBuilderForm: return run-job request
  AnonymizerBuilderForm->>useAnonymizerCreateRunJob: create run job
Loading

Possibly related PRs

Suggested reviewers: steramae-nvidia, aray12

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: anonymizer entity picker categories and custom labels.
✨ 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 astd-335-anonymizer-entity-picker/mschwab

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

@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.

Actionable comments posted: 4

🧹 Nitpick comments (2)
web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.test.ts (1)

189-231: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add coverage for the excluded-defaults + no-custom-labels case.

Once the buildDetectConfig gap noted in schema.ts is addressed, add a test asserting entityMode: 'custom', includeDefaultEntities: false, entityLabels: [] does not silently fall back to server defaults.

🤖 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 `@web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.test.ts` around
lines 189 - 231, The tests around buildAnonymizerJobRequest need coverage for
custom mode with defaults excluded and no selected labels. Add a case using
includeDefaultEntities: false and entityLabels: [] that asserts the detect
configuration explicitly represents an empty label selection rather than being
omitted or falling back to server defaults, alongside the existing custom-label
tests.
web/packages/studio/src/routes/AnonymizerBuilderRoute/components/EntitiesSection.tsx (1)

34-35: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate hook subscription with AnonymizerBuilderForm.tsx.

This component and AnonymizerBuilderForm.tsx both call useAnonymizerListEntityLabels(workspace, { query: {} }) independently. Likely deduped via react-query's cache since the query key matches, but consider lifting the fetch to the parent and passing data/isLoading down to avoid the duplicate subscription and keep a single source of truth.

🤖 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
`@web/packages/studio/src/routes/AnonymizerBuilderRoute/components/EntitiesSection.tsx`
around lines 34 - 35, Lift the useAnonymizerListEntityLabels subscription out of
EntitiesSection and AnonymizerBuilderForm into their shared parent, then pass
the fetched data and loading state to both components through props. Update
EntitiesSection’s available derivation to use the provided data, preserving the
existing empty-array fallback and single source of truth.
🤖 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.

Inline comments:
In
`@web/packages/studio/src/routes/AnonymizerBuilderRoute/components/AnonymizerBuilderForm.tsx`:
- Line 53: Gate form submission on the entity-label query completing so default
labels cannot be replaced by the interim empty array. Update the state derived
from useAnonymizerListEntityLabels and the Full Run button’s disabled condition,
and ensure the submit/buildDetectConfig path remains blocked while labels are
loading, alongside the existing createJob and model-loading checks.

In
`@web/packages/studio/src/routes/AnonymizerBuilderRoute/components/EntitiesSection.tsx`:
- Around line 94-105: Update the X icon in the selected-label rendering within
EntitiesSection to use the lucide-react `size` prop instead of `fontSize`,
preserving the intended small icon appearance and the existing Tag behavior.

In `@web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.ts`:
- Around line 182-187: Update the conditional in the label-building logic after
`labels` is deduplicated so it checks whether the merged labels contain any
custom `form.entityLabels`, rather than comparing `labels.length` with
`defaultEntityLabels.length`. Preserve returning `undefined` when only default
entities are selected, including when `defaultEntityLabels` contains duplicates,
while retaining custom labels in the request.
- Around line 176-190: Update buildDetectConfig so custom mode with
includeDefaultEntities=false and an empty form.entityLabels is rejected rather
than returning undefined and triggering backend defaults. Require at least one
custom label or preserve defaults enabled, while keeping existing behavior for
non-empty selections and default-inclusive configurations.

---

Nitpick comments:
In
`@web/packages/studio/src/routes/AnonymizerBuilderRoute/components/EntitiesSection.tsx`:
- Around line 34-35: Lift the useAnonymizerListEntityLabels subscription out of
EntitiesSection and AnonymizerBuilderForm into their shared parent, then pass
the fetched data and loading state to both components through props. Update
EntitiesSection’s available derivation to use the provided data, preserving the
existing empty-array fallback and single source of truth.

In `@web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.test.ts`:
- Around line 189-231: The tests around buildAnonymizerJobRequest need coverage
for custom mode with defaults excluded and no selected labels. Add a case using
includeDefaultEntities: false and entityLabels: [] that asserts the detect
configuration explicitly represents an empty label selection rather than being
omitted or falling back to server defaults, alongside the existing custom-label
tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: b58e6d3e-3fff-421d-a63d-e9dbfc7c7f89

📥 Commits

Reviewing files that changed from the base of the PR and between 1b3782c and 23fd3e8.

📒 Files selected for processing (7)
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/components/AnonymizerBuilderForm.tsx
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/components/EntitiesSection.tsx
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/constants.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/entityItems.test.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/entityItems.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.test.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.ts

Comment thread web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.ts
Comment thread web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.ts Outdated
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 27559/35314 78.0% 62.5%
Integration Tests 16094/34032 47.3% 19.8%

Comment thread web/packages/studio/src/routes/AnonymizerBuilderRoute/constants.ts Outdated

@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)
web/packages/common/src/components/form/ControlledCombobox/index.tsx (1)

47-62: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add an explicit return type to the exported component.

ControlledCombobox is a public, complex API. As per coding guidelines, “Use explicit return types for public APIs and complex functions in TypeScript.”

🤖 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 `@web/packages/common/src/components/form/ControlledCombobox/index.tsx` around
lines 47 - 62, Add an explicit JSX return type to the exported
ControlledCombobox component declaration, preserving its existing generic
parameter and implementation behavior.

Source: Coding guidelines

web/packages/common/src/components/form/ControlledCombobox/index.test.tsx (2)

6-10: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Order imports by group.

Move the @nemo/... import below external-library imports. As per coding guidelines, “Group imports: external libraries, internal modules, relative imports in TypeScript.”

🤖 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 `@web/packages/common/src/components/form/ControlledCombobox/index.test.tsx`
around lines 6 - 10, Reorder the imports in the ControlledCombobox test so
external-library imports come first, followed by the internal `@nemo/common`
import, preserving all existing imports and behavior.

Source: Coding guidelines


79-88: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test typing with an existing selection.

Starting with [] misses regressions that clear existing labels. Select email, type custom text, then assert the value remains ['email'].

🤖 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 `@web/packages/common/src/components/form/ControlledCombobox/index.test.tsx`
around lines 79 - 88, Update the test case “never writes raw input into a
multi-select field” to initialize the multi-select with the existing `email`
selection, then type custom text into the combobox and assert the value remains
exactly `['email']`. Preserve the array-type assertion while covering that
free-form input does not clear existing selections.
🤖 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 `@web/packages/common/src/components/form/ControlledCombobox/index.test.tsx`:
- Around line 6-10: Reorder the imports in the ControlledCombobox test so
external-library imports come first, followed by the internal `@nemo/common`
import, preserving all existing imports and behavior.
- Around line 79-88: Update the test case “never writes raw input into a
multi-select field” to initialize the multi-select with the existing `email`
selection, then type custom text into the combobox and assert the value remains
exactly `['email']`. Preserve the array-type assertion while covering that
free-form input does not clear existing selections.

In `@web/packages/common/src/components/form/ControlledCombobox/index.tsx`:
- Around line 47-62: Add an explicit JSX return type to the exported
ControlledCombobox component declaration, preserving its existing generic
parameter and implementation behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 7876af8a-e057-4700-8d02-6bab09dd35bb

📥 Commits

Reviewing files that changed from the base of the PR and between b02c6d8 and 3497604.

📒 Files selected for processing (9)
  • web/packages/common/src/components/form/ControlledCombobox/index.test.tsx
  • web/packages/common/src/components/form/ControlledCombobox/index.tsx
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/components/AnonymizerBuilderForm.tsx
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/components/EntitiesSection.tsx
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/constants.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/entityItems.test.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/entityItems.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.test.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/components/AnonymizerBuilderForm.tsx
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/entityItems.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/components/EntitiesSection.tsx
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/constants.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/entityItems.test.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.ts
  • web/packages/studio/src/routes/AnonymizerBuilderRoute/schema.test.ts

marcusds added 3 commits July 28, 2026 13:01
…ASTD-335]

Group the entity-label picker under the 13 curated categories, colour the
selections as chips, and allow labels outside the default set.

The entity-labels endpoint returns a flat list of 65 strings, so the
category grouping is a curated map in Studio, keyed to the same label ids
the API uses. Anything the API adds later that isn't mapped falls into an
Other bucket rather than disappearing from the menu.

Custom labels are offered as a synthetic "Custom label" item built from
whatever is typed, because the underlying combobox has no create
affordance and the shared ControlledCombobox forces an empty inputValue
in multi-select mode.

Include-defaults is no longer mutually exclusive with a custom selection:
the picker stays visible, the checkbox carries the live default count, and
the request merges the defaults with the picks since entity_labels
replaces the default set server-side.

Signed-off-by: mschwab <mschwab@nvidia.com>
Gate Full Run on the entity-labels query so a submit that races it can't
merge custom picks against an empty default set, which would silently
narrow detection to just those picks.

Reject custom mode with no labels and defaults excluded: that combination
sent no detect config at all, so the server fell back to its own defaults
instead of the restricted set Custom mode promises.

Compare the merged label count against the deduplicated default count, so
a duplicate in the defaults can't mask a genuinely new custom label.

Derive the chip colour type from Tag instead of restating its palette, and
size the chip icon with `size` since lucide ignores `fontSize`.

Signed-off-by: mschwab <mschwab@nvidia.com>
…335]

Now that the shared component accepts a caller-controlled inputValue
(ASTD-344), the picker no longer needs its own Combobox plus useController.
Chips read the selection through useWatch and remove via setValue, so the
field has a single registration.

Signed-off-by: mschwab <mschwab@nvidia.com>
@marcusds
marcusds force-pushed the astd-335-anonymizer-entity-picker/mschwab branch from 3497604 to d738273 Compare July 28, 2026 20:04
@marcusds
marcusds enabled auto-merge July 28, 2026 20:06
@marcusds
marcusds added this pull request to the merge queue Jul 28, 2026
Merged via the queue into main with commit e39c168 Jul 28, 2026
62 checks passed
@marcusds
marcusds deleted the astd-335-anonymizer-entity-picker/mschwab branch July 28, 2026 20:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants