Skip to content

fix(ui): virtual keys filter silently resets after navigating away an… - #26972

Merged
oss-pr-review-agent-shin[bot] merged 0 commit into
BerriAI:litellm_agent_oss_staging_05_07_2026from
Bytechoreographer:fix/virtual-keys-filter-persistence
May 7, 2026
Merged

fix(ui): virtual keys filter silently resets after navigating away an…#26972
oss-pr-review-agent-shin[bot] merged 0 commit into
BerriAI:litellm_agent_oss_staging_05_07_2026from
Bytechoreographer:fix/virtual-keys-filter-persistence

Conversation

@Bytechoreographer

Copy link
Copy Markdown
Contributor

PR: fix(ui): virtual keys filter silently resets after navigating away and back

Branch: Bytechoreographer:fix/virtual-keys-filter-persistence
Target: BerriAI:litellm_internal_staging
PR link: https://github.com/Bytechoreographer/litellm/pull/new/fix/virtual-keys-filter-persistence


Relevant issues

Pre-Submission checklist

  • npm run test passes for affected files (30/30)
  • Scope is isolated: 4 files changed, no new dependencies
  • Comment @greptileai and get Confidence Score ≥ 4/5 before requesting maintainer review

Type

🐛 Bug Fix

Changes

Root cause

On the Virtual Keys page, applying a filter (e.g. Key Alias) worked on
first click — but after navigating to another page and coming back, the
filter chips still showed the selected values while the table reverted to
all keys, as if the filter had never been applied.

The page ran two parallel data paths for the same list:

VirtualKeysTable
  ├─ useKeys(page, size, { sortBy, sortOrder, expand })        ← NO filters
  │    └─ result: paginated full key list  → passed as `keys` prop to useFilterLogic
  │
  └─ useFilterLogic({ keys, teams, organizations })
       ├─ filters:        useState<FilterState>   (filter UI state)
       ├─ debouncedSearch: one-off keyListCall with filters, writes filteredKeys
       └─ useEffect([keys, filters]):
             result = [...keys]
             if (filters["Team ID"])        result = result.filter(...)
             if (filters["Organization ID"]) result = result.filter(...)
             setFilteredKeys(result)        ← ONLY Team/Org branches, nothing else

Timeline of the bug:

  1. User types "foo" into Key AliasdebouncedSearch fires a filtered
    keyListCallfilteredKeys = server result ✓ the table shows the filtered rows.
  2. User navigates away. Component stays mounted or unmounts; either way
    useKeys eventually re-runs (staleTime: 30s expiry, storage event,
    window re-focus, remount).
  3. New keys prop arrives → the [keys, filters] effect re-runs →
    result = [...keys] (unfiltered) → Key Alias and User ID have no
    client-side filter branch
    , so setFilteredKeys(result) overwrites the
    server-filtered list with the full page.
  4. filters state is untouched, so the FilterComponent still shows "foo"
    — but the table displays everything.

Reproduce:

  1. On the Virtual Keys page, filter by Key Alias (e.g. "foo") → rows filter correctly ✓
  2. Click another page in the sidebar, wait ~30 s, click back to Virtual Keys
  3. Filter chip still shows "foo" but the table now lists all keys ✗
  4. Toggle the chip off and re-apply → rows filter again ✓ (one-shot)

Fix

Route filter values through the main useKeys query so React Query's cache
key tracks them. Same (page, filter, sort) tuple → same cache entry, which
means any remount or refetch naturally pulls the filtered page — the UI and
the data can no longer drift apart.

  const { data: keys, ... } = useKeys(page, size, {
    sortBy, sortOrder, expand: "user",
+   teamID:           debouncedFilters["Team ID"]         || undefined,
+   organizationID:   debouncedFilters["Organization ID"] || undefined,
+   selectedKeyAlias: debouncedFilters["Key Alias"]       || undefined,
+   userID:           debouncedFilters["User ID"]         || undefined,
  });

- const { filters, filteredKeys, filteredTotalCount, ... } = useFilterLogic({
-   keys: keys?.keys || [], teams, organizations,
- });
+ const { filters, ... } = useFilterLogic({ teams, organizations });

- data: filteredKeys,
+ data: keys?.keys ?? [],

- const totalCount = filteredTotalCount ?? keys?.total_count ?? 0;
+ const totalCount = keys?.total_count ?? 0;

A 300 ms debounce sits between filters and the values fed to useKeys
FilterComponent fires onApplyFilters on every keystroke of the text
inputs (Key Alias, User ID), so without debouncing each character would
trigger its own request. Pagination resets to page 1 when the debounced
filter values change, so a narrowed result set doesn't leave the user
stranded on an out-of-range page.

useFilterLogic is reduced to a pure state holder: filters,
handleFilterChange, handleFilterReset, plus the existing
allTeams / allOrganizations bootstrap. The debouncedSearch branch,
the [keys, filters] effect, filteredKeys, and filteredTotalCount
are all removed.

Files changed

File Change
ui/litellm-dashboard/src/components/key_team_helpers/filter_logic.tsx Drop debouncedSearch, the [keys, filters] effect, filteredKeys, and filteredTotalCount; remove the keys prop; handleFilterChange now just updates state
ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx Feed debounced filters (300 ms) into useKeys options; drive the table from keys?.keys; reset pagination to page 1 on filter change; remove the redundant skipDebounce hop on the sort handler
ui/litellm-dashboard/src/components/key_team_helpers/filter_logic.test.tsx Rewrite against the new contract; includes a regression test that filter state survives re-renders
ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx Update per-test mocks so table rows come from useKeys instead of filteredKeys; replace the two filteredTotalCount pagination cases with one server-filtered-total case and one assertion that filter values are forwarded into useKeys

@codecov

codecov Bot commented May 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a data/UI desync on the Virtual Keys page where applied filters stayed visible in the filter chips but the table silently reverted to all keys whenever React Query refetched in the background. The root cause was that useKeys had no awareness of the active filter values, so any refetch fetched the unfiltered list and the [keys, filters] effect overwrote the server-filtered result.

  • useFilterLogic is reduced to a pure state holder (filters, handleFilterChange, handleFilterReset, allTeams/allOrganizations); all debounced API calls and client-side filter effects are removed.
  • VirtualKeysTable debounces filter state (300 ms) and passes those values directly into useKeys, making the cache key encode the full (page, sort, filter) tuple so any remount or background refetch naturally reuses the same filtered query.
  • Pagination resets to page 1 when debounced filter values change; the reset handler now also restores sort state, addressing the previously noted gap.

Confidence Score: 5/5

Safe to merge — the change is well-scoped to the Virtual Keys UI layer, correctly routes filter state through the React Query cache key, and is covered by updated unit tests.

Moving filter values into the useKeys cache key means any refetch or remount uses the same (page, sort, filter) tuple, eliminating drift between filter chips and table data. Removed client-side filtering code and tests are correctly deleted rather than masked. No correctness issues found.

No files require special attention.

Important Files Changed

Filename Overview
ui/litellm-dashboard/src/components/key_team_helpers/filter_logic.tsx Stripped to a pure state holder: removes debouncedSearch, filteredKeys, filteredTotalCount, and the [keys, filters] sync effect; adds "Key Hash" to FilterState; handleFilterChange no longer accepts skipDebounce
ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx Feeds debounced filter values (300 ms) into useKeys so the cache key tracks filter state; drives table from keys?.keys; resets pagination to page 1 on filter change; reset handler now also restores sort state
ui/litellm-dashboard/src/components/key_team_helpers/filter_logic.test.tsx Rewritten against the new contract: replaces API-call assertions with pure state checks; adds regression test confirming filter values survive re-renders
ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx Per-test mockUseKeys now supplies the key data directly; adds two assertions that filter values (Key Alias, Key Hash) are forwarded into useKeys; removes filteredTotalCount tests for deleted functionality

Reviews (2): Last reviewed commit: "fix(ui): reset virtual keys sort on filt..." | Re-trigger Greptile

Bojun-Vvibe added a commit to Bojun-Vvibe/oss-contributions that referenced this pull request May 1, 2026
- BerriAI/litellm#26972 merge-after-nits: virtual keys filter routed through React Query cache

- BerriAI/litellm#26970 needs-discussion: Venice AI additions contaminated by databricks row churn + capability-flag drop

- BerriAI/litellm#26968 merge-after-nits: tighten router-settings-override fallback validation + mock-testing strip
@Bytechoreographer

Copy link
Copy Markdown
Contributor Author

@greptileai

@oss-pr-review-agent-shin
oss-pr-review-agent-shin Bot changed the base branch from litellm_internal_staging to litellm_agent_oss_staging_05_07_2026 May 7, 2026 02:09
@oss-pr-review-agent-shin
oss-pr-review-agent-shin Bot merged this pull request into BerriAI:litellm_agent_oss_staging_05_07_2026 May 7, 2026
42 checks passed
@oss-pr-review-agent-shin

Copy link
Copy Markdown
Contributor

🤖 litellm-agent: Squash-merged into staging branch litellm_agent_oss_staging_05_07_2026. Staging PR: #27359


Triage Summary
Refactors the virtual keys filter in the dashboard UI. Moves debouncing, API-driven filtering, and pagination reset logic from the useFilterLogic hook into VirtualKeysTable directly, so the hook now only manages filter state and loads teams/organizations. Removes the Sort By and Sort Order fields from FilterState and adds Key Hash. Updates tests to reflect the new data flow where useKeys receives debounced filter values rather than useFilterLogic performing its own API calls.

Merge Confidence: 5/5 ✅ READY
Ready to ship.

All checks green. Greptile 5/5, no blocking pattern findings, no CircleCI runs (OSS-typical).

@oss-pr-review-agent-shin

Copy link
Copy Markdown
Contributor

🤖 litellm-agent: Reverted from staging branch litellm_agent_oss_staging_05_07_2026. PR reopened for re-review.

@krrish-berri-2

Copy link
Copy Markdown
Contributor

merge reverted @Bytechoreographer can you please refile and include proof of fix working as expected (screen recording works)

@Bytechoreographer

Copy link
Copy Markdown
Contributor Author

merge reverted @Bytechoreographer can you please refile and include proof of fix working as expected (screen recording works)

20260507-120909 @krrish-berri-2 The former one is not fixed. When I changed to other tabs and then came back, the filter failed to work.

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.

2 participants