Skip to content

fix(ui): keep Virtual Keys list state in the URL so it survives leaving the page - #39481

Merged
yuneng-berri merged 5 commits into
litellm_internal_stagingfrom
litellm_virtual-keys-url-state
Sep 3, 2026
Merged

yuneng-berri merged 5 commits into
litellm_internal_stagingfrom
litellm_virtual-keys-url-state

Conversation

@yuneng-berri

@yuneng-berri yuneng-berri commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Virtual Keys forgets your search, sort and page
  • Stepping away from the page resets the list
  • A filtered key list cannot be shared or bookmarked

How it solves it:

  • Table state now lives in the URL query string
  • Browser back returns to the same filtered view
  • Sits beside the ?key= deep link already there

User Flow

Before: an admin filtering a long key list loses the filter every time they step away from the page

  1. They open https://litellm-domain/ui/?page=api-keys and type a key alias into the search box
  2. The list narrows to the matching keys, and the address bar still reads .../api-keys/
  3. They click Teams in the sidebar to check something, then press Back to return
  4. The search box is empty and the full unfiltered list is back, so they retype the search
  5. Pasting the page address to a teammate sends them the unfiltered list, not what was on screen

After: the same admin returns to the list exactly as they left it

  1. They open https://litellm-domain/ui/?page=api-keys and type a key alias into the search box
  2. The list narrows to the matching keys, and the address bar now reads .../api-keys/?key_search= followed by what they typed
  3. They click Teams in the sidebar to check something, then press Back to return
  4. The search box still holds their term and the same narrowed list is on screen
  5. Pasting that address to a teammate opens the same filtered list for them

Relevant issues

Linear ticket

Resolves LIT-3370

Pre-Submission checklist

  • I have added meaningful tests
  • The handful of test files covering my change pass locally
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review

Screenshots / Proof of Fix

Driven against a live proxy with the Admin UI dev server, signed in as a proxy admin, with 33 virtual keys present. Steps are the same on both sides.

Before (bcd3e2d)

Case 1: a filtered list survives leaving the page and coming back

  1. Search the Virtual Keys list for seed-key-1
address bar : /api-keys/
search box  : 'seed-key-1'
rows shown  : 1
  1. Click Teams in the sidebar, then press browser Back
address bar : /api-keys/
search box  : ''
rows shown  : 33          [filter lost]

Case 2: a filtered view opens from a pasted link

  1. Open /api-keys/?key_search=seed-key-1 directly, as if pasted from a teammate
address bar : /api-keys/?key_search=seed-key-1
search box  : ''
rows shown  : 33          [link ignored]

After (a223b76)

Case 1: a filtered list survives leaving the page and coming back

  1. Search the Virtual Keys list for seed-key-1
address bar : /api-keys/?key_search=seed-key-1
search box  : 'seed-key-1'
rows shown  : 1
  1. Click Teams in the sidebar, then press browser Back
address bar : /api-keys/?key_search=seed-key-1
search box  : 'seed-key-1'
rows shown  : 1           [filter kept]

Case 2: a filtered view opens from a pasted link

  1. Open /api-keys/?key_search=seed-key-1 directly, as if pasted from a teammate
address bar : /api-keys/?key_search=seed-key-1
search box  : 'seed-key-1'
rows shown  : 1           [link restores the view]

Hand-edited URLs, same browser session, at a223b76

These params are typed by hand now, so they are clamped and validated before they reach /key/list:

?page=0                      -> page=1&size=50
?page=-3                     -> page=1&size=50
?page_size=0                 -> page=1&size=1
?page_size=1000              -> page=1&size=100
?page=9223372036854775810    -> page=100000&size=50   (previously a backend 500)
?sort_by=totally_unknown     -> page=1&size=50&sort_by=created_at&sort_order=desc, 33 rows
?filter_user=%20user-42%20   -> user_id=user-42
?create=true&team_id=team-42 -> page=1&size=50, 0 filter chips, create modal open

Filter round trip and the key detail round trip:

apply User ID filter         -> ?filter_user=user-42
leave and come back          -> ?filter_user=user-42, chips restored
open ?page_size=10           -> Showing 1-10 of 12 | Page 1 of 2
click next page              -> ?page_size=10&page=2 | Page 2 of 2
leave and come back          -> ?page_size=10&page=2 | Showing 11-12 of 12
open a key, click Back       -> ?key_search=seed-key-3, search box still 'seed-key-3'

Search box input paths (click, type per character, blur, escape-then-type, paste): the value is never wiped, and one request fires per settled value.

Reviewer UI check, if you want to see it yourself: open http://localhost:4000/ui/?page=api-keys, type something into the key search box, click Teams in the sidebar, then press the browser Back button.

Type

🐛 Bug Fix

Caveats (if any)

Low

  • Filter and sort changes replace the history entry rather than pushing
    • Back leaves the page instead of undoing the last filter tweak
    • Pushing per keystroke would make Back unusable
  • A page number past the last page still prints an odd footer
    • Reachable only by hand-editing the address bar
    • The count math lives in the shared table, untouched here
  • The per-team keys table keeps its state in the component
    • Org-wide list now persists, per-team list still resets
    • Left alone as a separate follow-up

…ng the page

The search term, sort, pagination and drawer filters lived in component
state, so navigating away from Virtual Keys and back reset the table to an
unfiltered first page. Move them into query state alongside the existing
?key= deep link, which also makes a filtered view shareable.
The unprefixed team_id filter hijacked the /api-keys create-key deep link,
which already takes team_id as a prefill, so ?create=true&team_id=X silently
filtered the list underneath the modal. Prefix the four drawer filters.

Now that page and page_size come from the address bar, clamp them to what
/key/list accepts instead of forwarding 0, negatives or an int64-overflowing
page straight through, and trim filter values arriving from a URL the same
way the drawer already trims them.
… one

A hand-edited or stale sort_by reached /key/list, which 400s it, leaving the
Virtual Keys page on its loading skeleton with no error. Validate it against
the fields the table's own headers can produce, and clear sort_by rather than
blanking it when a sort is reset so the URL stays clean.

Also replaces a default-state URL assertion that ran before any query-state
write could land, so it could not fail for the regression it named.
@greptile-apps

greptile-apps Bot commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR moves the organization-wide Virtual Keys table’s search, sorting, pagination, and filters into validated URL query parameters so views survive navigation and can be shared.

  • Restores URL state on mount and updates it as table controls change.
  • Bounds pagination values and validates sortable fields before querying the backend.
  • Uses TanStack’s functionalUpdate for controlled table-state updater callbacks.
  • Adds regression coverage for persistence, filtering, sorting, pagination, and malformed query parameters.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx Replaces local table state with validated URL-backed state and correctly resolves TanStack controlled-state callbacks with functionalUpdate.
ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.test.tsx Adds comprehensive coverage for URL restoration, updates, defaults, bounds, filter clearing, and sortable-field validation without revealing a blocking regression.
ui/litellm-dashboard/src/components/VirtualKeysPage/keyTableColumns.tsx Exports the existing sortable-field allowlist so URL input can be validated against fields supported by the table and API.

Reviews (2): Last reviewed commit: "fix(ui): use TanStack's functionalUpdate..." | Re-trigger Greptile

…ater resolver

The local helper narrowed typeof updater === "function" against an
unconstrained T, which TypeScript cannot do because T itself may be a function
type, so next build failed to type check. table-core already exports the same
helper.

Copy link
Copy Markdown
Contributor Author

Last commit swapped the hand-rolled updater resolver for TanStack's own functionalUpdate to fix the build-ui type check.

@greptileai review


Generated by Claude Code

@devin-ai-integration

Copy link
Copy Markdown
Contributor

QA at a223b76 passed: search, sort, pagination, detail Back, malformed params and create=true all restore from the URL. Recording in Slack

Page 2 sorted, state in URL

More evidence

Restored after Teams and Back
page_size=1000 clamped to 100
sort_by=totally_unknown falls back to created_at desc
create=true modal, no filter params

@yuneng-berri
yuneng-berri merged commit 658f506 into litellm_internal_staging Sep 3, 2026
77 checks passed
@yuneng-berri
yuneng-berri deleted the litellm_virtual-keys-url-state branch September 3, 2026 07:35
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