Skip to content

fix(ui): limit incident Assignee filter to users/teams with incidents - #36402

Open
shah-harshit wants to merge 5 commits into
mainfrom
shah-harshit/issue-7693-fix-pr
Open

shah-harshit wants to merge 5 commits into
mainfrom
shah-harshit/issue-7693-fix-pr

Conversation

@shah-harshit

@shah-harshit shah-harshit commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Describe your changes:

Fixes open-metadata/openmetadata-collate#7693

The Incident Manager Assignee filter listed every user and team in the org, so picking someone with no incidents just returned an empty list.

The filter keeps the shared Teams / Users picker (UserTeamSelectableList): two tabs, counts on both, search and paging. It now only offers the users and teams assigned to at least one incident, in both the AI-mode grouped Incident Manager and the classic incident list.

  • UserTeamSelectableList takes an optional allowedIds. When set, every user/team search it runs (the lists, their searches, both tab counts) adds a terms clause on id.keyword. Unset leaves every other caller unchanged.
  • useIncidentAssigneeIds() reads those ids once per domain from a terms aggregation on testCaseResolutionStatusDetails.assignee.id over the testCaseResolutionStatus index (POST /search/aggregate). The options never depend on the page of incidents on screen.
  • Scoped to the active domain, with the same domains.fullyQualifiedName term/prefix the rest of the app uses.
  • While the ids load, or if the read fails, the picker offers no one rather than everyone. Ids that arrive while it is open re-read both lists and both counts.
  • The request sends deleted: null. Incident documents have no top-level deleted field, and the schema default deleted: false adds a term that matches none of them (verified against a local stack: deleted:false → 0 buckets, deleted:null → the real assignees).
  • The aggregation reads up to AGGREGATE_PAGE_SIZE_LARGE (1000) assignees; a terms aggregation cannot page.

Known limits (left as is on purpose):

  • The index keeps every status record, so an assignee who was later reassigned, or whose incidents are all resolved, still shows. Exact "current assignee of an open incident" would need a new endpoint over the SQL incident state table.
  • Status/severity/date filters do not narrow the options: they apply to the latest record of each incident, which a terms aggregation over the history cannot express.
  • On a table's Incidents tab the options cover the whole domain, not just that table.
  • The picker's search matches whole words, as it does everywhere else (Aaron finds Aaron Johnson; aa does not).

Type of change:

  • Bug fix

High-level design:

N/A — small change (3 UI files).

Tests:

Use cases covered

  • AI mode and classic: opening the Assignee filter shows the Teams and Users tabs, each counted and listing only incident assignees (sample data: Teams 0, Users 6, instead of 222 / 320).
  • Searching within a tab stays within that set.
  • With a domain selected, only that domain's incident assignees are offered.
  • Picking an assignee filters the incidents; clearing removes the filter.
  • While the assignees load, or if their read fails, the picker offers no one.
  • An assignee restored from the URL is still shown by display name.

Unit tests

  • UserTeamSelectableList.test.tsx: allowedIds scopes the user/team searches and counts; no ids leaves them unscoped; ids that arrive while the picker is open re-read both lists and both counts.
  • IncidentGroupsFilters.test.tsx (AI mode): aggregation request, ids reach the picker, domain scope, failure → nobody.
  • IncidentManager.test.tsx (classic): opening the picker searches users and teams scoped to the incident assignee ids.
  • npx jest src/components/observability/IncidentManager src/components/IncidentManager src/components/common/UserTeamSelectableList → 31 suites, 391 tests passed on b42b82497c6.
  • npx tsc --noEmit clean; make ui-checkstyle-changed clean.

Backend integration tests

  • Not applicable (no backend changes).

Ingestion integration tests

  • Not applicable (no ingestion changes).

Playwright (UI) tests

  • Not run locally (OSS PR). The existing classic-mode specs that pick through select-assignee keep working: the picker and its test ids are unchanged.

Manual testing performed

  1. Local stack (MySQL + Elasticsearch + server on :8585, sample data with incidents); this branch on the Vite dev server proxied to it.
  2. Aggregation on assignee.id returns 6 ids; the user index filtered by them returns those 6 users, the team index 0.
  3. AI mode → Observability → Incidents → Assignee: Teams 0 / Users 6; search Aaron → Aaron Johnson; pick → 7 groups become 1.
  4. Classic /incident-manager → Assignee: same tabs and counts; pick Aaron Johnson → his one incident.
  5. Same aggregation with deleted: false → no buckets (why the request sends null).

UI screen recording:

This branch at 6ccc83f929b, Vite dev server proxied to a local server on :8585 with sample data, 1600×1000. Before this PR, the same picker listed every team and user (Teams 222, Users 320 on this data).

AI mode (Observability → Incidents)

Opened (Teams tab) Users tab Search Aaron Picked
ai open ai users ai search ai picked

Classic (/incident-manager, Classic interface)

Opened (Teams tab) Users tab Search Aaron Picked
classic open classic users classic search classic picked

Recordings (H.264, about 16 s each): AI mode · classic

Checklist:

  • I have read the CONTRIBUTING document.

  • My PR title is Fixes <issue-number>: <short explanation> (title carries no ticket id by team convention; the ticket is linked above)

  • My PR is linked to a GitHub issue via Fixes above.

  • I have commented on my code, particularly in hard-to-understand areas.

  • For JSON Schema changes: not applicable.

  • For a UI feature or feature task: not applicable (bug fix).

  • I have added tests (unit / integration / Playwright as applicable) and listed them above.

  • Self-review: I reviewed the final diff against REVIEW.md and fixed every 🔴 finding, or answered it on the line below.

    • Reviewed b42b82497c6. No 🔴. Answered: historical assignees, filters that don't narrow the options and table-tab scope are listed above as known limits.
  • Decision check: no durable decision; nothing added under docs/decisions/.

  • I have added a test that covers the exact scenario we are fixing.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 4/5

Refresh the open Assignee picker when its IDs arrive before merging.

Findings

  1. P1 Assignees stay hidden after loading ▶

Summary

The PR limits Assignee choices using a domain-scoped aggregation of incident assignee IDs.

  • Incident assignee filters list only people found in incident records.

Diagram

sequenceDiagram
  participant View as Incident view
  participant Aggregate as Incident aggregation
  participant Picker as User/team picker
  participant Search as User/team search
  View->>Aggregate: Request assignee IDs for active domain
  View->>Picker: Supply empty IDs while loading
  Picker->>Search: Fetch choices if opened
  Aggregate-->>View: Return assignee IDs
  View->>Picker: Supply loaded IDs
  Note over Picker: Open list does not fetch again
Loading

Reviews (3) · Last reviewed commit: "fix(ui): keep the Teams/Users assignee p..." · Reviewed by Greptile

The AI-mode Incident Manager Assignee filter listed every user and team.
Read its options from a terms aggregation over the incident index instead,
scoped to the active domain, so only assignees of some incident show.
The aggregate request sends deleted: null because incident documents have
no top-level deleted field and the default deleted: false term matches none.
@shah-harshit shah-harshit added UI UI specific issues safe to test Add this label to run secure Github workflows on PRs skip-pr-checks Bypass PR metadata validation check labels Oct 28, 2026
@shah-harshit
shah-harshit requested a review from a team as a code owner October 28, 2026 06:41
@shah-harshit shah-harshit self-assigned this Oct 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

This PR cannot be merged until the following are addressed on its linked issue:

  • No GitHub issue is linked. Link an issue in the Development section of the PR (or add Fixes #12345 to the description). For a same-org cross-repo issue, add Fixes open-metadata/<repo>#123 to the description.

The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically.

Maintainers can bypass this check by adding the skip-pr-checks label.

@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Jest test Coverage

UI tests summary

Lines Statements Branches Functions
Coverage: 79%
79.77% (131387/164692) 66.28% (46331/69900) 69.24% (26304/37988)

@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ UI Checkstyle passed — lint findings in changed files

🔍 ESLint findings in this PR's files — 0 error(s), 3 warning(s)

Errors block the build. Warnings do not yet — they are rules whose backlog is still
being worked down, listed so this PR does not add to it. See docs/ui-code-quality-gate.md.

0 error(s), 3 warning(s) across 3 changed file(s).

Count Rule
2 openmetadata-imports/no-lower-layer-page-imports
1 react-hooks/exhaustive-deps
All findings
Location Rule Message
🟡 src/components/IncidentManager/IncidentManager.test.tsx:18:1 openmetadata-imports/no-lower-layer-page-imports Pages are route-level composition modules. Move the shared implementation/type to a lower layer instead of importing a page from here.
🟡 src/components/IncidentManager/useIncidentFilterOptions.ts:27:1 openmetadata-imports/no-lower-layer-page-imports Pages are route-level composition modules. Move the shared implementation/type to a lower layer instead of importing a page from here.
🟡 src/components/common/UserTeamSelectableList/UserTeamSelectableList.component.tsx:359:6 react-hooks/exhaustive-deps React Hook useEffect has a missing dependency: 'init'. Either include it or remove the dependency array.

Fix locally (fast - only checks files changed in this branch):

make ui-checkstyle-changed

…ignees

Restore the tabbed UserTeamSelectableList in the AI-mode incident filter
and give it an allowedIds prop: the user and team lists, their searches and
the tab counts only cover those ids. Both incident filters (AI mode and the
classic list) pass the ids of the incident assignees, read once per domain
from a terms aggregation on assignee.id.
})
),
};
}, [allowedIds]);

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.

P1 Assignees stay hidden after loading

Opening the Assignee picker before the aggregation finishes leaves the open list empty after the IDs arrive. Updating allowedIds rebuilds the query filters, but SelectableList fetches its first page only on mount. The picker also keeps that empty page until it closes, and its tab counts do not refresh.

Users must close and reopen the picker to see the loaded choices. Clear the cached pages and refresh the list and counts when allowedIds changes, or wait to mount the list until the IDs are ready.

Knowledge Base Used: UI foundations

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in b42b824, in UserTeamSelectableList itself so both incident filters get it. When allowedIds changes while the picker is open, both lists remount (their key is the ids) and fetch again, the cached first pages are dropped, and both tab counts are read again. Ids that arrive after opening now fill the open picker; no close and reopen needed. A new test checks this and fails without the fix. Both comments come from this one root cause, so there were no other occurrences to fix.

@sonarqubecloud

Copy link
Copy Markdown

),
};
}, [allowedIds]);
const allowedIdsKey = allowedIds?.join(',');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Bug: Remounted lists still read the old cache, so new ids show no one

Changing allowedIdsKey remounts both SelectableLists. Each one's mount useEffect calls fetchOptions('') (SelectableList.component.tsx, fetchListOptions). In a commit React runs child passive effects before parent ones, so this call happens before the parent effect at lines 384-395 clears initialUserOptionsRef/initialTeamOptionsRef. fetchUserOptions/fetchTeamOptions check the ref synchronously at the top (lines 162, 205), so they return the empty first page cached while the ids were []. The tab counts update to the new totals, but the lists stay empty until the picker is closed and reopened: the same bug this commit is meant to fix. The test misses this because it mocks SelectableList and calls fetchOptions by hand after the parent effect has run. Fix: clear the caches before any child effect runs, using useLayoutEffect (all layout effects in a commit run before passive effects), or store the cache together with the ids key it was built for and ignore it when the key differs.

Fix 1: Store the ids key with each cached page so a cache built for other ids is never returned (apply the same change to the team cache and fetchTeamOptions).
const initialUserOptionsRef = useRef<{
  key?: string;
  data: EntityReference[];
  paging: { total: number; after?: string };
} | null>(null);
// in fetchUserOptions:
if (isInitialPage && initialUserOptionsRef.current?.key === allowedIdsKey) {
  return initialUserOptionsRef.current;
}
// when caching:
initialUserOptionsRef.current = { ...result, key: allowedIdsKey };
  • Apply fix
Fix 2: Clear the caches in a layout effect so it runs before the remounted children's mount effects.
useLayoutEffect(() => {
  initialUserOptionsRef.current = null;
  initialTeamOptionsRef.current = null;
}, [allowedIdsKey]);
  • Apply fix

Check a box to apply a fix or reply for a change | Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Oct 28, 2026

Copy link
Copy Markdown
Code Review ⚠️ Changes requested 1 closed / 2 findings

🟡 Medium risk · The domain-scoped, 1,000-bucket assignee aggregation could omit valid users or teams, leaving them unavailable in incident filters.

Limits the incident Assignee filter to users and teams that appear on at least one incident, using a domain-scoped aggregation of assignee ids. Opening the picker before ids load now shows no one, which addresses the earlier finding. The verdict is changes_requested: when the ids change while the picker is mounted, both lists are remounted and their mount effect reads the stale cache, so newly loaded assignees stay hidden until the picker is closed and reopened. Fix by clearing the caches in a useLayoutEffect or keying the cache to the ids.

⚠️ Bug: Remounted lists still read the old cache, so new ids show no one

📄 openmetadata-ui/src/main/resources/ui/src/components/common/UserTeamSelectableList/UserTeamSelectableList.component.tsx:144 📄 openmetadata-ui/src/main/resources/ui/src/components/common/UserTeamSelectableList/UserTeamSelectableList.component.tsx:162-164 📄 openmetadata-ui/src/main/resources/ui/src/components/common/UserTeamSelectableList/UserTeamSelectableList.component.tsx:205-207 📄 openmetadata-ui/src/main/resources/ui/src/components/common/UserTeamSelectableList/UserTeamSelectableList.component.tsx:384-395 📄 openmetadata-ui/src/main/resources/ui/src/components/common/UserTeamSelectableList/UserTeamSelectableList.component.tsx:495 📄 openmetadata-ui/src/main/resources/ui/src/components/common/UserTeamSelectableList/UserTeamSelectableList.component.tsx:534 📄 openmetadata-ui/src/main/resources/ui/src/components/common/UserTeamSelectableList/UserTeamSelectableList.test.tsx:224-237

Changing allowedIdsKey remounts both SelectableLists. Each one's mount useEffect calls fetchOptions('') (SelectableList.component.tsx, fetchListOptions). In a commit React runs child passive effects before parent ones, so this call happens before the parent effect at lines 384-395 clears initialUserOptionsRef/initialTeamOptionsRef. fetchUserOptions/fetchTeamOptions check the ref synchronously at the top (lines 162, 205), so they return the empty first page cached while the ids were []. The tab counts update to the new totals, but the lists stay empty until the picker is closed and reopened: the same bug this commit is meant to fix. The test misses this because it mocks SelectableList and calls fetchOptions by hand after the parent effect has run. Fix: clear the caches before any child effect runs, using useLayoutEffect (all layout effects in a commit run before passive effects), or store the cache together with the ids key it was built for and ignore it when the key differs.

Store the ids key with each cached page so a cache built for other ids is never returned (apply the same change to the team cache and fetchTeamOptions).
const initialUserOptionsRef = useRef<{
  key?: string;
  data: EntityReference[];
  paging: { total: number; after?: string };
} | null>(null);
// in fetchUserOptions:
if (isInitialPage && initialUserOptionsRef.current?.key === allowedIdsKey) {
  return initialUserOptionsRef.current;
}
// when caching:
initialUserOptionsRef.current = { ...result, key: allowedIdsKey };
Clear the caches in a layout effect so it runs before the remounted children's mount effects.
useLayoutEffect(() => {
  initialUserOptionsRef.current = null;
  initialTeamOptionsRef.current = null;
}, [allowedIdsKey]);
✅ 1 closed
✅ Edge Case: Opening the picker before assignee ids load shows an empty list

📄 openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/useIncidentFilterOptions.ts:70-80 📄 openmetadata-ui/src/main/resources/ui/src/components/common/UserTeamSelectableList/UserTeamSelectableList.component.tsx:120-134 📄 openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/IncidentGroupsFilters.tsx:178 📄 openmetadata-ui/src/main/resources/ui/src/components/observability/IncidentManager/IncidentGroups/IncidentGroupsFilters.tsx:227-232 📄 openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx:85 📄 openmetadata-ui/src/main/resources/ui/src/components/IncidentManager/IncidentManager.component.tsx:226
useIncidentAssigneeIds returns [] while the aggregation is still in flight, and UserTeamSelectableList turns that into terms: { 'id.keyword': [] }, which matches no users or teams. SelectableList fetches its options only once, on mount (useEffect(() => fetchListOptions(), [])), and the tab counts are read once in init when the popover opens. So if the user opens the Assignee picker before the aggregation returns (slow index, cold cache), the list and counts stay at zero for that whole session, even after assigneeIds arrives, and look the same as "no incident has an assignee". One fix is to pass undefined/disable the trigger until the query succeeds and separately treat a loaded empty result as empty. Another is to add allowedIds to the SelectableList key so it remounts and refetches when the ids arrive.

🤖 Prompt for agents
Code Review: Limits the incident Assignee filter to users and teams that appear on at least one incident, using a domain-scoped aggregation of assignee ids. Opening the picker before ids load now shows no one, which addresses the earlier finding. The verdict is changes_requested: when the ids change while the picker is mounted, both lists are remounted and their mount effect reads the stale cache, so newly loaded assignees stay hidden until the picker is closed and reopened. Fix by clearing the caches in a useLayoutEffect or keying the cache to the ids.

1. ⚠️ Bug: Remounted lists still read the old cache, so new ids show no one
   Files: openmetadata-ui/src/main/resources/ui/src/components/common/UserTeamSelectableList/UserTeamSelectableList.component.tsx:144, openmetadata-ui/src/main/resources/ui/src/components/common/UserTeamSelectableList/UserTeamSelectableList.component.tsx:162-164, openmetadata-ui/src/main/resources/ui/src/components/common/UserTeamSelectableList/UserTeamSelectableList.component.tsx:205-207, openmetadata-ui/src/main/resources/ui/src/components/common/UserTeamSelectableList/UserTeamSelectableList.component.tsx:384-395, openmetadata-ui/src/main/resources/ui/src/components/common/UserTeamSelectableList/UserTeamSelectableList.component.tsx:495, openmetadata-ui/src/main/resources/ui/src/components/common/UserTeamSelectableList/UserTeamSelectableList.component.tsx:534, openmetadata-ui/src/main/resources/ui/src/components/common/UserTeamSelectableList/UserTeamSelectableList.test.tsx:224-237

   Changing `allowedIdsKey` remounts both `SelectableList`s. Each one's mount `useEffect` calls `fetchOptions('')` (SelectableList.component.tsx, `fetchListOptions`). In a commit React runs child passive effects before parent ones, so this call happens before the parent effect at lines 384-395 clears `initialUserOptionsRef`/`initialTeamOptionsRef`. `fetchUserOptions`/`fetchTeamOptions` check the ref synchronously at the top (lines 162, 205), so they return the empty first page cached while the ids were `[]`. The tab counts update to the new totals, but the lists stay empty until the picker is closed and reopened: the same bug this commit is meant to fix. The test misses this because it mocks `SelectableList` and calls `fetchOptions` by hand after the parent effect has run. Fix: clear the caches before any child effect runs, using `useLayoutEffect` (all layout effects in a commit run before passive effects), or store the cache together with the ids key it was built for and ignore it when the key differs.

   Fix (Store the ids key with each cached page so a cache built for other ids is never returned (apply the same change to the team cache and fetchTeamOptions).):
   const initialUserOptionsRef = useRef<{
     key?: string;
     data: EntityReference[];
     paging: { total: number; after?: string };
   } | null>(null);
   // in fetchUserOptions:
   if (isInitialPage && initialUserOptionsRef.current?.key === allowedIdsKey) {
     return initialUserOptionsRef.current;
   }
   // when caching:
   initialUserOptionsRef.current = { ...result, key: allowedIdsKey };

   Fix (Clear the caches in a layout effect so it runs before the remounted children's mount effects.):
   useLayoutEffect(() => {
     initialUserOptionsRef.current = null;
     initialTeamOptionsRef.current = null;
   }, [allowedIdsKey]);

Verification

Rules No rules evaluated

Functional validation Not enabled

Set up

Auto-approval Not enabled

Set up

For more verification details, comment gitar display:verbose.

Options

Display: compact → Showing compact verification results.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

safe to test Add this label to run secure Github workflows on PRs skip-pr-checks Bypass PR metadata validation check UI UI specific issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant