Commit graph

65 commits

Author SHA1 Message Date
yuneng-jiang
93c1461074
fix(ui): restore hover feedback and dark-mode variants lost in the token migration (#37579)
* fix(ui): restore hover feedback and dark-mode variants lost in the token migration

PR #37576 mapped hardcoded Tailwind palette classes onto semantic tokens. Two-tone
hover pairs collapsed onto a single token, so 116 hover utilities across 49 files
became identical to their base class and produced no visible feedback, and in seven
files a dark: variant was dropped while its hardcoded light partner survived, leaving
those elements stuck light in dark mode.

Hover states now follow the alpha-step idiom the shadcn primitives already use
(hover:bg-primary/80, hover:bg-success/20): a duplicated hover:text-X or hover:bg-X
becomes /80, hover:border-border becomes hover:border-ring, and a duplicate is
dropped where another hover utility on the element already carries the change. One
transition-colors that no longer animated anything is removed.

For the dark-mode gaps, indigo maps onto info and amber onto warning. There is no
purple token in globals.css, so the purple sites keep their palette classes and get
their dark: partner back.

* fix(ui): add an eslint rule that fails a hover: utility identical to its base

The token migration collapsed two-tone hover pairs by hand, so nothing catches
the next one. `local/no-noop-hover-variant` reads every string literal and
template chunk and errors when a `hover:X` sits alongside a bare `X`, which is
exactly the shape that renders no hover feedback. It ships at error with no
suppression baseline, so the eleven sites that already carried a dead hover
before the migration are fixed here too.

The rule reads one class string at a time, so a base class supplied by a
different ternary branch than its hover partner is left alone: a selected row
whose resting colour already matches its hover colour is deliberate, not a bug.
2026-08-19 22:52:36 -07:00
yuneng-jiang
663e647bc8
refactor(ui): migrate antd Modal onto the shared shadcn Dialog (#37540)
* test(ui): cover the two modals no test would catch breaking

Both files sit in the antd Modal migration's blind spot. EditSSOSettingsModal's
test replaced antd wholesale with a stub Modal and asserted the stub's own
data-testid markup, so it proved nothing about the modal a user sees and would
have stayed green through any regression. routing_groups had no test at all.

Rewrite the first against the real antd Modal, querying by dialog role and
accessible name so the assertions hold under either library, and add an
integration test for the second that drives the row menu and the delete
confirmation end to end.

Modal width drops out of the SSO assertions: antd carries it as an inline style
and shadcn as a max-width class, so either form couples the test to the library
rather than to anything a user perceives.

* refactor(ui): move the straightforward antd Modals onto the shared Dialog

Twenty files whose Modal only used title, open, width, footer, className and
onCancel, so each one maps onto Dialog without judgement calls. Width becomes a
max-width class, the body gets the house scroll cap so tall content stays
reachable, and destroyOnHidden goes away because Base UI unmounts a closed
dialog on its own.

EditMembership needed a real fix rather than a translation. Clearing the form
after a submit resolved only ever worked by accident: the reset set every field
to undefined, which react-hook-form does not push out to a subscribed
Controller, and the fields looked cleared only because antd's Modal happened to
re-render the subtree afterwards. Dialog does not, so the stale values showed
through. emptyMemberFormValues now returns the empty value each control
actually understands, an empty string, null or an empty list, and the reset
lands whatever renders around it. Its test asserted the undefined shape while
describing the behaviour it was missing, so it now checks the values instead.

* refactor(ui): migrate the antd Modals that needed a judgement call

Sixteen more files. Most carried a prop that does not translate literally:
maskClosable becomes disablePointerDismissal, afterOpenChange becomes
onOpenChangeComplete, and closable={false} becomes showCloseButton={false}.

The styles prop went away everywhere it appeared. All but one instance set the
body to 24px and the header to 24px with no border, which is what DialogContent
already renders, so keeping it would have meant writing the default back by
hand.

Several Modals passed onOk alongside footer={null}, so antd rendered no OK
button and the handler could never fire. Each of those handlers was a
character-for-character copy of the neighbouring onCancel, so they are gone
rather than translated.

Rich titles now sit inside DialogHeader with DialogTitle carrying the heading
text, instead of the whole header block being nested inside DialogTitle. That
had put an h2 inside another h2, which is invalid and gave one dialog two
headings.

UserEnvVarsModal loses its formGeneration counter. Remounting the form when the
modal finished opening only mattered because antd kept a closed modal's
children mounted; Base UI unmounts them, so reopening is blank on its own. Its
test helper had encoded that remount as a timing assumption, so the file now
states the requirement outright and checks that reopening shows an empty field.

CreateMCPServer's cancel test read the tool list while the modal was closed,
which only worked because forceRender kept it mounted. It now asserts what a
user can actually observe: the panel is gone while closed, and reopening brings
back an empty URL and no tools.

Unmounting an open Base UI dialog leaves its scroll lock on <html> and <body>,
which survives cleanup() and makes every later test in the file see a locked
page where popups compute pointer-events: none and clicks quietly do nothing.
The shared setup now releases it.

* refactor(ui): finish the antd Modal migration onto the shared Dialog

Fifteen files whose Modal relied on antd's built-in footer. okText, cancelText,
onOk, okButtonProps, cancelButtonProps and confirmLoading collapse into two
explicit buttons in a DialogFooter, with danger becoming the destructive
variant and the various loading flags becoming disabled plus aria-busy. The one
okButtonProps that also hand-set a red background drops it, since the variant
already carries that.

add_guardrail_form keeps its own chrome, so its DialogContent turns off the
built-in close button and the padding, and its heading becomes the DialogTitle.
Under antd it passed title={null} and had no accessible name at all.

Modals that positioned themselves near the top of the viewport needed
translate-y-0 alongside top-8, because DialogContent centres itself with a
transform that top alone does not undo.

TeamGuardrailsTab's test reached its Mode select by index into every combobox on
the page. A modal dialog hides the rest of the page from assistive technology,
which antd never did, so the count changed and the index pointed at the wrong
control. It asks for the field by label now.

The mask-dismissal test drove antd's .ant-modal-wrap class directly; it uses the
overlay slot our own component exposes, and still fails if
disablePointerDismissal is dropped.

CreateUserButton stays on antd. Its Modal converts cleanly, but the colocated
test file then fails a varying handful of cases, and the cause sits in the test
file rather than the component, so it wants its own change.

Pruning suppressions for the touched files also cleared four
react-hooks/set-state-in-effect entries on CreateMCPServer that were already
stale before this branch.

* test(ui): pick Base UI select options through the shared helper

React 19's flush timing loses the race this test was relying on: the option
lands in the DOM one render before its positioner drops pointer-events: none,
so user-event refused the click. tests/test-utils already exports
chooseSelectOption for exactly this, added alongside the React 19 upgrade.
2026-08-19 23:16:42 +00:00
ryan-crabbe-berri
f1e143a87c
chore(ui): upgrade the dashboard to React 19 (#37411)
* chore(ui): upgrade the dashboard to React 19

Bumps react and react-dom from 18.3.1 to 19.2.8 with matching @types. Next 16 already required a React 19 peer, so this aligns the dashboard with what the framework expects and unblocks Base UI and shadcn work that assumes the React 19 ref model.

React 19 passes ref through as a regular prop, so the setup file's forwardRef tripwire and the ref-forwarding test's forwardRef case no longer describe real behavior; both now assert the React 19 contract instead. useRef<T>(null) now yields RefObject<T | null>, which is the one prop type MessageList had to widen.

* test(ui): wait for a Base UI select popup to open before clicking an option

The option lands in the DOM one render before the popup finishes entering, while its positioner still carries pointer-events: none, so clicking it throws. Waiting on the option's text alone was a race that React 19's flush timing loses, which is why four ToolPolicies cases went red on the bump.

chooseSelectOption in test-utils opens the trigger, finds the option by role, waits for it to stop being pointer-blocked, then clicks. It also replaces the last-match-by-text hack, which only worked because the popup happens to portal after the table.
2026-08-19 21:18:08 +00:00
yuneng-jiang
7675ba8717
test(ui): split the vitest suite into unit, component, integration and type projects (#37488)
* test(ui): split the vitest suite into unit, component and integration tiers

Every test file booted jsdom, including the ~1800 that assert pure functions
and never render. They now run as a separate vitest project in the node
environment, where the whole tier finishes in under four seconds.

The tiers are vitest projects rather than a naming convention, so CI can run
them as independent jobs. A .test.ts that renders React, a hook test being the
usual case, is listed explicitly and stays in the jsdom tier.

* test(ui): report per-test duration against a per-tier budget

A timeout only catches a hung test, and it has to stay generous enough to
survive a loaded runner, so it never reports the multi-second render tests that
make CI fail the moment the box is busy. Budgets are separate and far tighter:
50ms unit, 1s component, 3s integration.

The counts are laptop measurements, so the CI job is report-only for now.
Flipping it to blocking is one line once CI has published its own numbers.

* test(ui): run the tiers as separate CI jobs and stop clicking popups by text

The old job ran every file in one process, so the single slowest file set the
wall clock and a bigger box bought nothing. The tiers now run as separate jobs
with the component tier sharded four ways.

getByText and findByText match hidden nodes, so they resolve against a closed
Base UI popup whose positioner still carries pointer-events: none, and the
click lands or not depending on how far the open transition got. Two files
failed this way, one three runs in five and one every run. Querying the option
by role waits for it to be visible, and both are now stable. A lint rule keeps
the pattern from coming back.

* test(ui): give React Testing Library's async queries a CI-sized window

findBy* and waitFor run on asyncUtilTimeout, which defaults to 1000ms and is
independent of vitest's testTimeout. Raising the vitest timeout therefore did
nothing for them: a query still gave up after one second while the test had 59
seconds of budget left, which is why a loaded runner produced 'Unable to find
role=...' rather than a timeout.

UserSearchModal is the worked example. The role query it makes resolves in
249ms on a laptop and blew past 1000ms on CI, failing the run at 1494ms. Five
seconds keeps the same assertions and only widens the window a failing query
waits before reporting; a passing query still resolves the moment the element
appears.

* test(ui): calibrate the tier budgets from real CI numbers and report by default

The first CI run showed the laptop counts were badly off: component 176 local
against 326 on CI, integration 87 against 128. The maxima now come from that
run with headroom.

continue-on-error still painted the check red, which is the opposite of the
point, so the report-only decision moves into test-budgets.json as an explicit
enforce flag. The job passes and prints the counts; flipping enforce to true
makes it a gate.

* docs(ui): drop the CLAUDE.md edits from the tier split

Keeping this PR to the vitest, CI and test changes.

* ci(ui): run every tier in one job instead of eight check rows

Sharding bought nothing. Measured on the first run of this branch, the
component tier unsharded finishes in 198s while the integration tier is floored
at 384s by a single file, so integration was always the critical path and the
four component shards only added rows. One job running every project comes in
around 384s against the 426s the split jobs took.

Eight rows named things like 'component (2)' also told a reviewer nothing, on a
PR page that already carries forty checks.

The job keeps the id ui-unit-tests because guard-internal-staging requires that
exact context; renaming the jobs had silently stopped it reporting, which would
have blocked every merge on a check that no longer existed. The workflow's
display name becomes UI Tests since it runs more than unit tests.

The tier split itself is untouched: it lives in the vitest projects config, so
the unit tier still runs in node with no jsdom, and each tier keeps its own
timeout and budget.

* fix(ui): stop the type check from running the whole suite a second time

test:types was 'vitest --run --typecheck.only'. Under test.projects that flag
is ignored and the root-level typecheck block is not inherited, so the step
collected each project's normal include and ran all 8464 runtime tests instead
of type-checking. It took 542s on CI against 33s on the flat config it
replaced, and the job then ran the same suite again in the next step.

Typecheck now belongs to a project of its own, with an empty include so it
contributes no runtime tests, and the CI job runs one vitest invocation for all
four. The type tier adds about 3s to a full run and reports 'Type Errors: no
errors' rather than a suite of tests.

Verified it still catches things: breaking SortingState in DataTable.test-d.tsx
fails with 'Type number is not assignable to type string' and exit 1, and
restoring it passes.

* test(ui): scope the split down to the vitest tier projects

Removes everything from this branch that was not the tier split.

The three lint rules brought 1381 lines of grandfathered suppressions in
eslint-suppressions.json, which is 81% of the branch's added lines and
debt nobody is going to pay down. The per-test duration budget does not
scale as a CI step. Both are gone, along with the two query rewrites the
no-click-by-text rule forced: those files pass 10/10 at this base, quiet
and under load, so there was no failure behind them.

The workflow is byte-identical to the base again. It already runs
npm run test:types and then vitest related on pull requests, so PR cost
is unchanged; the split only repoints test:types at the new project.
That project is required, not optional: vitest silently ignores
--typecheck.only under test.projects, so without it the type script
collects the whole suite instead of the one typed file.

Restores the base 60s testTimeout on the unit tier. The 5s cap was not
part of the split and failed ChatShell.serverRootPath.test.ts, a 960ms
test, under load.
2026-08-19 20:40:14 +00:00
yuneng-jiang
5a899f596b
refactor(ui): port the add model form off antd Form onto react-hook-form (#37446)
* fix(ui): restore the cache control Role and Index field hints

The add_model cache control editor lost both field hints when it moved off
antd Form.Item in #37392. "LiteLLM will mark all messages of this role as
cacheable" and "(Optional) If set litellm will mark the message at this index
as cacheable" went with the Form.Item tooltip props and neither string exists
in dashboard source any more. The Index hint was the only thing telling a user
that field is optional, so this is lost information rather than styling.

Both come back as shadcn tooltips beside their labels, matching how the
surviving switch-level hint is already rendered.

Also adds the payload characterization net this graph did not have. Before
this commit the seven suites over add_model and model_add held 37 cases, no
antd module mock, and zero toStrictEqual, so nothing pinned the submit
payload. AddModelPanel.integration.test.tsx drives the real panel, the real
antd store and the real prepareModelAddRequest, and asserts the object handed
to modelCreateCall.

It pins the distinctions only a strict assertion can see: litellm_credential_name
arrives as null from its initialValue while api_key, api_base, mode and
access_groups arrive as undefined, and team_id is absent entirely until the
Team-BYOK switch mounts it. It also pins the mount gate in both directions,
since a collapsed Advanced Settings drops both its keys and anything typed
into it while re-expanding restores them, and the empty-string skip, since a
cleared api_base must vanish rather than arrive as "".

Every fixture was captured from the running component rather than written by
hand. A 12-mutation battery over the bindings, the empty-string skip, the two
required rules and an added keepMounted all go red, each run gated on having
executed the expected case count.

* refactor(ui): port the add model form off antd Form onto react-hook-form

The Add Model form graph is shared by three antd hosts, so it only moves as
one piece: AddModelPanel, LlmCredentialsPanel and CredentialModal all mount
the same children. Form and Form.Item are replaced everywhere, and every
widget inside them is left alone, so the change is the binding layer only.

antd submits the mounted fields, react-hook-form submits its whole store. A
shared mount registry keeps that difference from reaching the request: each
field registers on mount, and the panel projects the store down to the
registered names before it builds the payload. shouldUnregister would have
been the other option, but it drops a collapsed section's typed values, so
re-expanding Advanced Settings would come back empty.

The antd rules modules are reused as-is through a thin validator adapter, so
the messages stay in one place rather than being reworded per field.

Advanced Settings held a Form.useForm() instance in a component that renders
no Form, which made ten imperative calls dead. They are removed rather than
translated, and the three behaviours they looked like they drove were checked
against the antd original first: invalid LiteLLM Params still blocks submit,
the pass-through toggle still leaves LiteLLM Params empty, and turning custom
pricing off then on still keeps the typed cost.

The existing 14 case payload net runs unedited against the port.

* test(ui): pin the three add model behaviours the dead form instance looked like it drove

Advanced Settings used to hold a form instance it never rendered, and the ten
imperative calls against it were dead. The inherited payload net covered none
of the three behaviours those calls appeared to own, so removing them looked
riskier than it was. These cases characterise what the antd original actually
did, checked against it before the port.

Invalid LiteLLM Params blocks the submit, which also closes the one mutation
the inherited net could not kill: dropping the JSON rule left all 14 green.
2026-08-18 23:43:54 -07:00
ryan-crabbe-berri
bb8324c119
refactor(ui): drop @tremor/react and the theming scaffolding it needed (#37394)
The last tremor component import left the dashboard when the primitive
sweep merged, so the package, its v3 compatibility shim, its @theme token
block and the palette safelist it needed at runtime all have no consumer.

Removing the safelist is what shrinks the shipped stylesheet: tremor built
class names at runtime, so Tailwind had to emit every bg/text/border/ring/
stroke/fill utility across 22 palettes and 11 shades in case one was used.
Nothing in the app constructs a class name that way any more, so the
scanner finds every utility on its own.

The date-fns overrides pin also goes. It only existed because tremor and
react-day-picker@8 peered on date-fns 3 while Base UI wanted 4, and the
lockfile still resolves a single hoisted 4.4.0 without it.
2026-08-18 17:38:12 -07:00
ryan-crabbe-berri
087d82ffca
refactor(ui): move the model info view and pass-through endpoint forms off tremor (#37308)
* refactor(ui): move the model info view and pass-through endpoint forms off tremor

The pass-through settings form's Save button relied on tremor's implicit
submit, so it now carries an explicit type="submit" because the Base UI
button defaults to type="button". The include-subpath switch inside the
antd Form.Item is wired through onCheckedChange plus form.setFieldsValue,
since the Base UI switch does not read the onChange antd injects. Both
tab strips keep every panel mounted so the edit forms survive a tab
switch, pinned by a new mount-contract test on the model info view. Also
prunes the six tremor no-restricted-imports suppressions these files no
longer need.

* test(ui): pin the model info overview panel to its own dom node across tab switches

* fix(ui): keep the line tab strip on the model info and pass-through views

Both tab strips were bare tremor TabLists, which defaulted to the line
variant, so the straight rename turned them into filled segmented pills.
They now use the same full-width line strip the agent, guardrail and
prompt info views ship.
2026-08-18 23:19:23 +00:00
ryan-crabbe-berri
83ae623733
refactor(ui): codemod every toast call site onto lib/toast and delete the antd-era facades (#37253)
MessageManager and NotificationManager were thin facades over lib/toast since #37207. This
rewrites their ~750 call sites (226 files) to import { toast } from @/lib/toast directly:
success/info/warning/error keep their names, fromBackend becomes fromError, destroy/clear
become dismiss. The one config-object caller (CreateMCPServer's admin-review branch) becomes
an explicit toast.success(message, { description }). Behaviour is unchanged: no production
caller passed a duration, so every toast keeps the same kind, title and default duration.

Tests: the global vitest mock now targets @/lib/toast (toast.test.ts opts back out with
vi.unmock), so the per-file vi.mock boilerplate for the facades is deleted and assertions read
toast.success / toast.fromError. The two facade files, their test and their filename-case
suppressions are removed, along with the commented-out facade calls left in networking.tsx
2026-08-18 18:24:49 +00:00
tin-berri
1648273469
feat(ui): configure the auto router's heuristic scorer from the Admin UI (#37216)
* feat(ui): configure the auto router's heuristic scorer from the Admin UI

The complexity router has always read tier_boundaries, token_thresholds and
dimension_weights from its config, and /model/new already persists them, but the
dashboard had no control for any of the three, so tuning the scorer meant editing
config.yaml by hand.

Adds an "Advanced scoring" panel to the classification section, shown whenever the
scorer actually runs: on a heuristic router, and on an LLM classifier that falls back
to the heuristic. An untouched knob is omitted from the payload, so a router keeps
tracking the shipped defaults instead of freezing today's numbers.

The three keys join MANAGED_COMPLEXITY_ROUTER_KEYS, so the edit modal now rebuilds
them from form state rather than carrying the stored copy through. That makes
hydration load-bearing, and it hydrates an absent knob to undefined rather than to
the defaults, so an untouched save cannot pin a router that was tracking them.

The "How Classification Works" card now reads the configured boundaries instead of
hardcoding 0.15 / 0.35 / 0.60, which would otherwise start lying the moment an
operator changed them.

* test(complexity_router): pin the dashboard scorer defaults against config.py

The Admin UI keeps its own copy of the boundary, threshold and weight defaults to
prefill its controls. The copy is display only, since an untouched knob is omitted
from the payload, so drift shows a stale placeholder rather than pinning a router.
Nothing caught that drift before, and a blank or dead control is worse, so the two
copies and the dimension key set are pinned against each other here.

* fix(ui): surface out-of-order scorer thresholds as an error, not a hint

Boundaries that decrease make the tiers between them unreachable, which silently
changes where traffic goes, so amber body text undersold it. Saving stays allowed:
a router configured this way in config.yaml would otherwise become uneditable in
the UI for every unrelated change.

* fix(test): search the default-model picker instead of trusting option order

The pinned model is appended after every model the presets contribute, and that list
has reached 11, so the option fell outside the virtualized dropdown's rendered slice
and the two default-model-pin cases failed on staging. CI only runs them when this
file is touched, which is why they went unnoticed. Searching for the model filters
the list to it, so the cases no longer depend on how long the preset list grows.

* revert(test): drop the dashboard scorer defaults parity test

It parsed TypeScript from Python with a hand-rolled brace matcher and a numeric
literal regex, which is not a mechanism this repo should carry: two review rounds
went into fixing the parser rather than the feature. The UI copy of the defaults is
display only, since an untouched knob is omitted from the payload, so drift shows a
stale placeholder and cannot pin a router.

* fix(ui): clamp the scorer inputs and drive the panel from one group spec

min and max are inert attributes on a text input, so the fields accepted a weight of
999, a boundary of -50, and Infinity, and persisted them into the router config.
Values are now clamped on commit and non-finite input is refused.

The three sections were near copies of each other, so they now render from a single
group spec, which also removes the triplicated warning logic.

Moves the scorer constants and types into heuristic_scoring_knobs, the leaf module.
Reading them back through ComplexityRouterConfig was a cycle, so the top-level
DIMENSION_KEYS.map in the panel ran while the constant was still undefined and every
test importing it failed to collect.

* feat(ui): serve the scorer defaults from the proxy instead of mirroring them

The dashboard kept its own copy of DEFAULT_TIER_BOUNDARIES, DEFAULT_TOKEN_THRESHOLDS
and DEFAULT_DIMENSION_WEIGHTS to prefill the Advanced scoring controls. Two copies of
one fact, and the earlier attempt to police the gap parsed TypeScript from a Python
test, which was worse than the problem.

GET /public/complexity_router/scorer_defaults now returns them, following the
/public/providers/fields pattern: a typed response model, the dashboard fetching it
through a react-query hook next to useProviderFields. The controls and the "How
Classification Works" card both read that, so a recalibration of the defaults can no
longer leave the form stating numbers the router stopped using.

The dimension set now comes from the proxy too, so a dimension added backend-side
renders without a dashboard change, under its raw key until it is given a label.
Hydration keeps a stored dict exactly as stored rather than filling it from a local
copy, since the backend already defaults any key omitted at scoring time.

* fix(types): type the scorer defaults response as Mapping, not dict

LIT001 gates mutable collections in annotations, and the three dict fields tripped it.
Mapping is what the codebase already uses for a read-only map on a response model, and
the endpoint hands the config constants over directly rather than copying them into a
fresh dict, which would have traded the LIT001 hit for a LIT002 one.

* test(ui): stub the scorer defaults request for the auto-router tree

The Advanced scoring panel and the classification card read the shipped defaults over
the network, so every render of that tree in a test paid for a request jsdom cannot
serve. That was enough to push the slowest default-model-pin case past its 30s timeout
on CI, where the suite runs 14 forks in parallel.

One fixture in tests/mocks, pulled in by a single vi.mock line per test file, rather
than the same stub pasted into each of the seven that render the tree.

* fix(ui): tell a failed scorer-defaults load apart from a slow one

The panel read only the query's data, so a permanent failure was indistinguishable
from a request still in flight and it sat on "Loading the shipped defaults..." for
good. It now branches on the query state: pending says loading, an error says so and
offers a retry, and the values the router already overrides stay visible and editable
either way.

Two more places had the same flaw. The classification card silently dropped the tier
ranges it used to always show, and now says they could not be loaded. The weight total
was summed over whatever keys were present, so a failed load made it state a total
built from the overrides alone; a total is only shown when the dimension set is known.
2026-08-18 01:10:13 +00:00
ryan-crabbe-berri
2dc49a913c
refactor(ui): replace hand-rolled query-param routing with nuqs (#35871)
* refactor(ui): replace hand-rolled query-param routing with nuqs

The dashboard carried five copies of the same pushState-based detail
routing hook plus a shared navigateWithParams helper, each with its own
plumbing test and a copy-pasted reactive useSearchParams mock in
component tests. nuqs provides the same shallow history-API routing
behind useQueryState/useQueryStates, so the key, team and org hooks are
deleted in favor of inline useQueryState at their single consumers,
while the models and logs hooks keep their interfaces but drop their
hand-rolled internals. Component tests now mount NuqsTestingAdapter
(via renderWithProviders or locally) instead of patching window.history,
and URL assertions go through onUrlUpdate spies that can additionally
distinguish push from replace, which the old window.location checks
could not

* test(ui): assert browser back closes the log drawer after in-drawer selection

Greptile flagged that the nuqs port of the switching-logs test stopped
at asserting emitted push and replace modes. The test now replays those
recorded modes against a history stack and performs the back step, so a
regression to push-on-select or broken URL-derived drawer state fails
the test instead of passing silently
2026-08-05 13:29:58 -07:00
yuneng-jiang
03de6280b6
Merge pull request #35802 from BerriAI/litellm_/modest-pascal-71b7b2
refactor(ui): inject the fetch client's base url instead of reading it at import
2026-08-04 19:34:39 -07:00
yuneng-jiang
487074f602
chore(build): move the Admin UI toolchain to Node 24 (#35801)
* chore(build): move the Admin UI toolchain to Node 24

Node 18 and Node 20 both reached end of life (2025-04-30 and 2026-04-30), and
the release images along with every CI lane were still building on them. Node 24
is the current LTS through 2028-04-30, so this moves the four UI build images,
the CircleCI lanes, and the four GitHub Actions workflows onto it

Node 24 also ships npm 11.17, which is the first line that implements the
min-release-age setting this repo already carries in its .npmrc files. On npm 10
the key is parsed and discarded, so the release-age gate has had no effect
regardless of its value. Tightening the dashboard's engines range and turning on
engine-strict makes an unsupported npm fail loudly rather than skip the gate
quietly, and a new step in the UI build workflow probes an impossible cooldown
so an inert setting cannot pass unnoticed again

Node 24's bundled undici tightened its brand check on RequestInit.signal, which
rejects the AbortSignal jsdom installs and broke the two cases in
src/lib/http/api.test.ts that rebase a request onto a runtime base url. Under
jsdom the Request global comes from Node while AbortSignal comes from jsdom;
tests/jsdomFetchEnv.ts delegates to the jsdom environment and then restores
Node's native AbortController and AbortSignal so both come from one realm.
Upgrading jsdom does not address this, as jsdom still does not own Request

The workflows now read ui/litellm-dashboard/.nvmrc instead of repeating a
literal, so the Node version has a single source of truth, and ui/Dockerfile is
pinned by digest to match the other three build images. The lockfile changes are
npm 11 normalising the engines range and dropping optional peer entries it no
longer records

* fix(build): point every Admin UI build script at .nvmrc

The enterprise Docker path was left on Node 18. docker/build_admin_ui.sh runs
only when enterprise/enterprise_ui/enterprise_colors.json is present, which it
never is in the OSS tree, so neither CI nor a default image build reaches it;
it pinned nvm to v18.17.0 and then built the dashboard, which now requires Node
24, so a customized enterprise image would have failed EBADENGINE

All three UI build scripts now resolve the version from
ui/litellm-dashboard/.nvmrc rather than carrying their own pin, so the Node
version has a single home across Docker, CI, and local builds. build_ui.sh was
on v20 and build_ui_custom_path.sh on v18.17.0

Also drops the dependency-cooldown probe from the UI build workflow. The
engines floor plus engine-strict already fails an unsupported npm loudly at
install time, so the probe was redundant, and treating any nonzero exit from a
live registry call as proof of enforcement made it unsound besides
2026-08-04 12:36:07 -07:00
Yuneng Jiang
a6b9cedd03
refactor(ui): inject the fetch client's base url instead of reading it at import
api.ts read globalThis.location when the module loaded, which froze the base
URL at import and pinned its test file to jsdom. The creation-time baseUrl and
the middleware's runtime rebase were also two mechanisms doing overlapping
work, and the rebase hand-copied eleven RequestInit fields on every call.

Pass openapi-fetch's Request option instead, so the constructor applies
whatever getRequestBaseUrl() returns at the moment the request is built.
registerBaseUrlGetter is now the single source of the base URL, rebaseUrl and
rebaseRequest are deleted, and the request is constructed once, so the init
openapi-fetch assembled reaches the platform Request untouched. The abort
signal is no longer copied by hand.

This preserves behaviour rather than approximating it: getProxyBaseUrl() falls
back to location.origin, so the runtime base was never empty in a browser and
the old middleware already rebased every request, discarding the creation-time
value each time.

setupTests.ts gates its DOM-only tail behind a window check; setup files run
for every environment, so that tail previously stopped any node-environment
test file from loading.

api.test.ts now runs under @vitest-environment node with its assertions intact
and no location stub, plus regressions for per-call base resolution and abort
forwarding. api.sameOrigin.test.ts covers the browser fallback to the page
origin, which needs a DOM environment.
2026-08-04 12:07:45 -07:00
ryan-crabbe-berri
67fce87b16
chore(ui): add filename, size, JSX-handler, prefer-const, and antd lint rules (#34341)
* chore(ui): add filename, size, JSX-handler, prefer-const, and antd lint rules

Wires up five error-level ESLint rules on the dashboard, grandfathering every
current offender into eslint-suppressions.json so the gate only bites new code
and ratchets down as files are fixed

- local/filename-pascal-case: new local rule requiring PascalCase .tsx names,
  exempting Next.js reserved files (page, layout, route, ...) and test/spec files
  (239 grandfathered)
- max-lines: 800 lines over src/**, excluding tests, src/data, and generated
  schema.d.ts (20 grandfathered)
- local/no-complex-jsx-arrow: new local rule flagging inline JSX arrow handlers
  with block bodies over two statements; each failure is a small extract-to-named
  -handler refactor (65 grandfathered)
- prefer-const: flipped from off to error (103 grandfathered)
- no-restricted-imports: added antd to the phase-out ban alongside tremor, and
  pointed both messages at shadcn/ui primitives (405 antd import sites grandfathered)

Both new local rules ship with RuleTester coverage

* fix(ui): preserve secondary extensions in filename-pascal-case suggestion

The suggestion text built the rename from only the head segment, so a
multi-dot file like my-component.utils.tsx was told to become
MyComponent.tsx instead of MyComponent.utils.tsx. Rebuild it from the
PascalCased head plus the untouched remaining segments, and add tests
covering multi-dot filenames and the hyphenated Next.js reserved names
(global-error, apple-icon, opengraph-image, twitter-image)
2026-07-22 19:34:35 -07:00
Yuneng Jiang
2e492f5cb7
fix(ui): hide guardrail group headers when only one group has entries
The team settings guardrails dropdown always rendered the Global and
Other headers, so a proxy with no global guardrails showed an empty
Global heading above the list.
2026-07-18 16:27:59 -07:00
ryan-crabbe-berri
74ff8d0ff9
fix(ui): navigate to /ui/login/ with trailing slash via hard navigation (#33561)
* fix(ui): navigate to /ui/login/ with trailing slash via hard navigation

Logged-out redirects targeted /ui/login without the trailing slash, so
Starlette's StaticFiles(html=True) mount answered with a 307 whose
absolute Location is built from the scheme the container sees. Behind a
TLS-terminating reverse proxy uvicorn does not trust X-Forwarded-Proto
by default, so the redirect downgraded https to http and stranded users
on an unreachable URL (#33454). The auth guard also used the Next client
router for this navigation, which first requests an RSC payload that the
static export cannot serve, producing 404s before falling back to a full
page load.

Centralize the login URL in getLoginUrl(), which always emits the
trailing slash so no server redirect fires, and use
window.location.replace for the login redirects so no RSC fetch is
attempted.

* test(ui): expect trailing slash in expired-token login redirect
2026-07-16 12:18:55 -07:00
ryan-crabbe-berri
b6dbda48f9
refactor(ui): colocate the mcp-servers view, keeping the shared mcp_tools surface (#32968)
* refactor(ui): colocate the usage view, keeping the shared usage components

Split for the usage (UsagePage) segment. Most of the folder is the usage page's
own view, but four pieces are reused elsewhere and stay in @/components/UsagePage:
TopKeyView (old-usage), KeyModelUsageView and value_formatters (activity_metrics),
and the shared types (activity_metrics, chartUtils). The other 21 files move into
usage/_components, preserving the folder structure.

The external consumers import only the retained files, so they are untouched. The
moved files' imports of the retained files become @/components/UsagePage paths,
other escaping relative imports are absolutized, and lint suppressions are re-keyed
for moved files only. No behavior change.

* refactor(ui): colocate the mcp-servers view, keeping the shared mcp_tools surface
2026-07-11 18:01:41 -07:00
Yuneng Jiang
7cdf42d770
chore(ui): remove eslint-metrics.json lint-count snapshot
The eslint-metrics.json snapshot duplicated the violation counts already
enforced by eslint-budgets.json. Keeping it current added a CI drift check,
a pre-commit regenerate-and-flag step, and a standalone npm run lint:metrics
script, none of which caught anything the budget gate did not, yet all of
which failed noisily whenever the snapshot went stale. This drops the file
and that machinery while leaving eslint-budgets.json as the actual ratchet
gate
2026-07-11 11:54:42 -07:00
ryan-crabbe-berri
77a30a120c
refactor(ui): colocate agents and guardrails views, keeping shared selectors and types (#32728)
* refactor(ui): colocate agents and guardrails views, keeping shared selectors and types

The last two group-1 colocation splits. Both are file-plus-folder combos: the
page view is a top-level file (agents.tsx / guardrails.tsx) sitting beside a
supporting folder of the same name, and part of that folder is shared.

agents: agents/types (imported by the agents hook) stays in @/components/agents;
agents.tsx and the rest of the agents/ folder move into agents/_components (the
view file becomes _components/index.tsx).

guardrails: GuardrailSelector (imported by the Playground, the key edit view,
and the agents add-guardrail form) and types stay in @/components/guardrails;
guardrails.tsx and the other 47 folder files (including the tool_permission,
custom_code, content_filter, and llm_judge subfolders) move into
guardrails/_components.

Moved files' imports of the retained shared files become @/components paths,
other escaping relative imports are absolutized against @/, and moved test
files have their from/vi.mock/vi.importActual paths rewritten to match. Lint
suppressions are re-keyed for moved files only (the staying selector/types keep
their src/components keys). The shared selectors did not move, so their external
consumers are untouched. No behavior change.

* fix(ui): move the orphaned agents/guardrails view tests and drop dead GuardrailItem

Greptile follow-ups on the agents/guardrails colocation:

- agents.tsx and guardrails.tsx moved to their _components/index.tsx, but their
  sibling test files (src/components/agents.test.tsx, guardrails.test.tsx) were
  left behind still importing ./agents and ./guardrails, which broke a full
  vitest run. Move them to the matching _components/index.test.tsx and rewrite
  their view import to ./index, folder mocks to local ./ siblings, and
  networking to @/components/networking.
- Remove the unused GuardrailItem interface and its GuardrailDefinitionLocation
  import from the guardrails view (dead code carried over from before the move;
  state is typed as Guardrail[]).
2026-07-10 10:57:12 -07:00
ryan-crabbe-berri
3d5d5e1295
refactor(ui): colocate tag-management and vector-stores views, keeping the shared selectors (#32719)
Two of the group-1 colocation splits. Each of these folders lived in the shared
src/components dump but is only partly shared: the page's management view is
segment-owned, while a selector widget is reused by other features. So this
splits them rather than moving wholesale.

tag-management: TagSelector (used by playground) and its types stay in
@/components/tag_management; the management view (index, tag_info, TagTable,
CreateTagModal) moves to tag-management/_components.

vector-stores: VectorStoreSelector (used by organizations and playground) and
its types stay in @/components/vector_store_management; the rest of the
management UI moves to vector-stores/_components.

The moved files' imports of the retained shared files are rewritten to absolute
@/components paths, escaping relative imports are absolutized, and moved test
files have both their `from` imports and `vi.mock` paths rewritten to match.
Grandfathered lint suppressions for moved files are re-keyed. The external
consumers of the selectors are untouched (the selectors did not move). No
behavior change.
2026-07-09 22:19:11 -07:00
ryan-crabbe-berri
592510ec18
feat(ui): shadcn charts foundation with tremor-compatible wrappers (#32668) 2026-07-09 18:18:52 -07:00
ryan-crabbe-berri
65d90fd5cf
refactor(ui): colocate 11 route segments' components into _components/ (#32704)
Colocation follow-up to the App Router migration: move each page's owned
components out of the shared src/components dump and into its route segment's
_components/ folder, draining the shared bucket. Convention: a component used
by exactly one segment goes in that segment's _components/ (private, matching
Next's _ route-exclusion); a component shared by 2+ segments stays in
@/components. No new _shared/ folder.

Rename-in-place (segment already had a local components/ folder):
- api-reference (also relocates the shared CodeBlock, used by playground and
  cost-tracking, to @/components/CodeBlock)
- memory, budgets, access-groups
- caching, projects, guardrails-monitor

Extract from src/components (page view lived in the shared dump):
- AdminPanel -> admin-panel, organizations -> organizations,
  general_settings -> router-settings, usage -> old-usage

Each folder/view was verified to have no importer other than its own page
(cross-checked across src, tests, and e2e_tests). Relative imports inside moved
single files are rewritten to absolute @/components/*; colocated tests move with
their subject and have their vi.mock paths rewritten to match. Grandfathered
lint suppressions (tremor, react-hooks, and similar, all pre-existing) are
re-keyed to the new paths with counts unchanged. No behavior change.
2026-07-09 17:43:33 -07:00
ryan-crabbe-berri
7d63b86e00
fix(ui): forward refs through ui primitives and fail tests on swallowed refs (#32401)
* fix(ui): forward refs through ui primitives and fail tests on swallowed refs

Under React 18 a ref passed to a plain function component is dropped
with only a dev console warning, so Base UI render-prop triggers
composed over our shadcn-style primitives silently stop working (the
tooltip just never opens; ui/badge.tsx hit exactly this on the shared
DataTable branch). Label, Separator, Skeleton, UiLoadingSpinner and the
Table family now use React.forwardRef like Button and Input already
did, a contract test pins ref delivery for each, and setupTests turns
React's ref warning into a test failure so the next primitive that
swallows a ref fails CI instead of shipping a dead tooltip

* fix(ui): include captured ref warnings in the tripwire error

The afterEach tripwire threw a fixed message and discarded the collected
React warnings, so a failure never said which component swallowed the ref.
Append the captured warnings (component name + stack) to the thrown error.
2026-07-09 11:59:16 -07:00
ryan-crabbe-berri
5973d9fd2b
feat(ui): add eslint rules for nested ternaries, large inline object args, and long condition chains (#32415)
* feat(ui): add eslint rules for nested ternaries, large inline object args, and long condition chains

Adds three dashboard lint rules to keep new code readable. Nested ternaries
are banned outright via the built-in no-nested-ternary, with the 265 existing
occurrences grandfathered in eslint-suppressions.json so only new ones fail.

Two custom rules ship as a small local plugin under scripts/eslint-rules:
no-large-inline-object-arg flags object literals with 4+ properties passed
straight into a call, nudging toward a named variable, and no-long-condition-chain
flags boolean expressions that combine 4+ conditions, nudging toward a named
boolean. Both are warnings tracked on the existing budget ratchet
(eslint-budgets.json + eslint-metrics.json) with headroom above the current
counts, so they ratchet down over time rather than freezing a baseline. Both
thresholds are configurable rule options and covered by RuleTester unit tests.

* fix(ui): scope no-long-condition-chain to boolean operators, not nullish

Greptile flagged that the rule counted nullish-coalescing chains the same as
&&/|| chains, so a 4-part `a ?? b ?? c ?? d` fallback surfaced "Boolean
expression combines 4 conditions", which is inaccurate since a `??` fallback
is value defaulting, not a condition. Restrict the visitor to && / || nodes so
`??` chains are treated as leaves, while a boolean chain nested inside a `??`
is still caught. Drops 6 miscounted occurrences (240 -> 234).

* chore(ui): sync lint metrics and suppressions with staging

Merge advanced the base branch, adding one no-large-inline-object-arg
occurrence (508 -> 509) and making one grandfathered react-hooks suppression
stale. Regenerate eslint-metrics.json and prune the suppression so the
budget/drift gate passes.

* chore(ui): sync lint metrics with staging

Merge advanced the base, adding four no-large-inline-object-arg occurrences
(509 -> 513). Regenerate eslint-metrics.json so the drift gate passes.
2026-07-08 21:32:16 +00:00
ryan-crabbe-berri
3d644e1f9d
refactor(ui): colocate users page into route-level _components (#31897)
Moves the user-management component tree (view_users plus BulkEditUsers, edit_user, DefaultUserSettings, user_edit_view, and the view_users table/columns/info-view) out of the shared src/components dump into the users route segment under _components, now that the app router owns the route. The page imports from a trimmed ./_components barrel

UserInfo moves into networking.tsx beside UserListResponse, its real owner: networking defines the user API response shapes that embed it, and previously reached up into a view folder (components/view_users/types) to import the type. Defining it in networking removes that backwards data-layer-to-view dependency and drains the view_users/ folder entirely. CreateUserButton and onboarding_link stay in components/ since the create-key flow also consumes them

Relative imports in the moved files are rewritten to @/components/* absolute paths, and the eight pre-existing eslint-suppressions entries are re-keyed to the new paths so the move stays behavior and lint neutral

Verified: the moved suites pass with the same 75 assertions as before the move, tsc and eslint are clean, and next build compiles the /users route
2026-07-01 20:14:07 -07:00
ryan-crabbe-berri
f2f6cacb19
feat(ui): track frontend lint counts in a committed snapshot (#31157)
Some checks are pending
LiteLLM Rust / rustfmt, clippy, test (push) Waiting to run
GitHub Actions Security Analysis / zizmor (push) Waiting to run
* feat(ui): track frontend lint counts in a committed snapshot

Persist the eslint budget-rule counts (no-explicit-any, complexity,
max-depth) to eslint-metrics.json so the trend is queryable straight
from git history and can later feed a dashboard. A CI drift check
regenerated from the same lint report keeps the snapshot honest, so a
PR that shifts a count has to run npm run lint:metrics and commit it

* fix(ui): harden lint-metrics drift check and eslint failure handling

Make the drift comparison symmetric over the union of committed and
actual keys so a phantom rule left in eslint-metrics.json (for example
after a rule is dropped from eslint-budgets.json) is caught instead of
silently passing. Only swallow eslint's lint-errors exit code in the
generator and rethrow anything else, so a fatal eslint failure surfaces
its real output rather than a confusing ENOENT on the missing report
2026-06-24 11:35:32 -07:00
ryan-crabbe-berri
4def6916da
refactor(ui): consolidate dashboard to one shell in the (dashboard) layout (#30166)
* refactor(ui): consolidate dashboard to one shell in the (dashboard) layout

Moves the legacy ?page= switch page into the (dashboard) route group and
hoists Navbar, sidebar, ThemeProvider, and DebugWarningBanner into the
shared layout with real props, deleting the degraded duplicate shell that
wrapped migrated routes. The active page key now derives from the URL at
render time, so navigating between legacy and migrated pages no longer
remounts the shell.

useProxySettings becomes a React Query hook taking accessToken, shared by
the navbar, the AdminPanel arm, and migrated pages; this replaces the
lifted proxySettings state and the Navbar setProxySettings prop drilling.
The invitation onboarding flow (?invitation_id=) keeps rendering without
chrome via a layout escape hatch. Dead dark mode state and the no-op antd
ConfigProvider are removed.

* fix(ui): include accessToken in useProxySettings query key

The queryFn closes over accessToken, so the key must include it for the
cache to be honest about its inputs. Settings are instance-global today,
which made the omission harmless, but a token change while mounted would
have served the cached entry without refetching.

* test(ui): point CreateKeyPage test at the moved page

The page moved into the (dashboard) route group and no longer renders
the navbar (the layout owns chrome now), so the valid-token test asserts
the default page content (UserDashboard stub) instead.
2026-06-10 18:37:44 -07:00
ryan-crabbe-berri
248176112e
feat(ui): add admin flag to disable in-product UI nudges for everyone (#29796)
* feat(ui): add admin flag to disable in-product UI nudges for everyone

Admins can now suppress the survey and Claude Code feedback popups for
all users via a single disable_ui_nudges UI setting, instead of relying
on each user dismissing them individually.

* fix(ui): suppress nudges while ui settings are loading

Gate nudgesDisabled on the ui-settings loading state so an admin with
disable_ui_nudges on doesn't see the survey prompt flash, and the
getInProductNudgesCall fetch doesn't fire, on a cold page load before
the flag resolves. Falls back to showing nudges if the fetch errors.

* test(ui): wrap CreateKeyPage test in QueryClientProvider

page.tsx now calls useUISettings (react-query), which needs a
QueryClient that layout.tsx supplies in production but the test did
not. Add the provider and mock getUiSettings so the query resolves.
2026-06-09 17:45:42 -07:00
ryan-crabbe-berri
f3811ce63b
refactor(ui): shared HTTP client + location-pinned fetch() lint rule (#29723)
* refactor(ui): add shared HTTP client and pin raw fetch() to one file

Introduce src/lib/http/client.ts, a single typed wrapper that owns the only
fetch() in the dashboard. It centralizes the base URL, the auth header, error
parsing (deriveErrorMessage), non-2xx -> thrown ApiError, and JSON parsing, and
is framework-agnostic (no React) so it can run from client and, later, server
components. The base URL, auth header name and the logout side effect are injected
through createApiClient.

networking.tsx builds one configured apiClient and the 29 functions whose
boilerplate maps exactly to the client's default behavior (canonical
deriveErrorMessage + handleError + res.json() template) now call it instead of
hand-rolling fetch. Names, signatures, return types and error behavior are
unchanged; this is a pure refactor that drops ~440 lines.

The no-restricted-syntax fetch rule now points at the client and a
files: ["src/lib/http/**"] override makes that the only place fetch() is allowed.
Re-baselined eslint-suppressions.json: networking.tsx fetch suppressions drop
270 -> 241; no other rule's counts change.

The remaining networking.tsx fetches and the ~61 scattered component/hook fetches
diverge from the default client behavior (text() error bodies, no res.ok check,
no handleError side effect) and stay grandfathered for a follow-up burndown.

* fix(ui): make the HTTP client tolerate non-JSON error bodies

The non-2xx branch parsed the error body with response.json(), so a gateway
returning HTML (502/503 from a reverse proxy) threw a SyntaxError before onError
fired or ApiError was built, dropping the user-facing notification. This matched
the old per-function behavior, but the client is now the single error path so it
is the right place to harden. Read the body as text once, try JSON.parse for the
existing deriveErrorMessage path, and fall back to the raw text (or the HTTP
status) otherwise. The success path stays strict json() so return types are
unchanged.

* fix(ui): await the returned apiClient promise in 6 migrated functions

The codemod rendered the `return response.json()` tail as `return apiClient.x()`
without `await`. Inside the surrounding try/catch that returns an unawaited
promise, so the catch never runs and its console.error log is dropped on failure;
4 of the 6 were `return await response.json()` originally, so this restores their
exact behavior. Use `return await apiClient.x()` in all six.

* refactor(ui): widen onError type and handle empty success bodies

Address review notes on the shared client. Type onError as
(message: string) => void | Promise<void> so the fire-and-forget async contract
(networking passes the async handleError) is explicit rather than silently
discarded by void. On the success path, read the body as text and return
undefined for an empty body (e.g. a 204 No Content) instead of throwing a
SyntaxError, while still parsing non-empty bodies strictly so a malformed JSON
response surfaces rather than being masked. Add tests for the 204 case.
2026-06-04 20:27:58 -07:00
ryan-crabbe-berri
7edf3a9cb5
style(ui): run prettier --write across the dashboard (#29622)
Formatting-only pass; no logic changes. Brings the UI into compliance
with .prettierrc so the new format-check CI job passes
2026-06-04 11:37:54 -07:00
ryan-crabbe-berri
73e9071311
refactor(ui): extract auth state into AuthContext (#28910)
Some checks are pending
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Waiting to run
Unit Tests: Proxy DB Operations / auth-checks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / budgets (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / custom-logging (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / db-and-spend (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / key-generation (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / logging-misc (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-runtime (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-server-core (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / schema-migration (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-utils (push) Blocked by required conditions
Unit Tests: Security / security (push) Waiting to run
* refactor(ui): extract auth state into AuthContext

Move auth state (token, userID, userRole, accessToken, premiumUser, userEmail,
disabledPersonalKeyCreation, showSSOBanner) out of src/app/page.tsx into a
new AuthProvider at src/contexts/AuthContext.tsx. Wrapped at the root layout
so login/onboarding/dashboard routes all have access via useAuth().

Day 1 foundation for the App Router migration: migrated (dashboard)/X/page.tsx
route entry points won't have a parent passing props, so shared auth state
must live in a context they can read from.

Sub-components are unchanged — they still receive accessToken/userID/userRole
as props from page.tsx (which now reads them from useAuth()). Only the
page.tsx → top-level-page-component handoff is de-drilled; deeper prop
drilling is left for the per-page migration to address.

Net change: -86 lines from page.tsx (state + two effects moved), +5 in
layout.tsx (provider wrap), new AuthContext.tsx (~140 lines), test update
to wrap CreateKeyPage in AuthProvider.

Fixes LIT-3366
Part of LIT-3128

* fix(ui): await getUiConfig before clearing authLoading

The AuthContext refactor flipped authLoading to false synchronously on mount
while letting getUiConfig() run fire-and-forget. On SERVER_ROOT_PATH deployments
this races the unauthenticated login-redirect effect: the redirect fires with
proxyBaseUrl still at its module-init value, sending users to /ui/login instead
of {SERVER_ROOT_PATH}/ui/login.

Restores the original sequencing inside AuthProvider's mount effect and adds a
Playwright spec wired into the existing SERVER_ROOT_PATH workflow matrix. The
spec delays the config endpoint via page.route() to make the race deterministic
across CI runners.
2026-05-26 17:53:03 -07:00
ryan-crabbe-berri
727a471ae9
[Refactor] UI - Spend Logs: consolidate filter state and extract components (#25847)
* [Refactor] UI - Spend Logs: consolidate filter state, extract components, remove dead code

- Lift filter state into index.tsx and pass to hook (removes selectedX vars + sync useEffect)
- Move main useQuery into useLogFilterLogic hook (removes isMainQueryEnabled toggle)
- Delete dead RequestViewer component (300 lines, replaced by LogDetailsDrawer)
- Extract LogsTableToolbar component (search, date range, pagination, live tail)
- Extract filter options config to filter_options.ts
- Remove dead code: handleRefresh, handleSelectLog, handleCloseDrawer, formatTimeUnit,
  showFilters/showColumnDropdown state, dropdownRef/filtersRef

* Fix PR feedback: use antd Switch instead of Tremor in new file, fix typo

* Collapse dual-path filtering into single React Query

All 10 filter keys now go through the useQuery — the imperative
performSearch / debouncedSearch / backendFilteredLogs path is deleted.
Filter values are debounced via useDebouncedValue(300ms) before hitting
the query key so text inputs don't fire per-keystroke.

Removed: performSearch, debouncedSearch, backendFilteredLogs,
lastSearchTimestamp, hasBackendFilters, clientDerivedFilteredLogs,
the sort/page/time refetch useEffect, and the filteredLogs chooser memo.

* Clean up remaining smells: remove isFetchingDeferred, internalize selectedTimeInterval, fix circular import

- Remove useDeferredValue/isButtonLoading — pass logsQuery.isFetching directly
- Move selectedTimeInterval into LogsTableToolbar as internal state
- Move PaginatedResponse type from index.tsx to log_filter_logic.tsx

* Fix quick-select dropdown overlapping sidebar

* Fix stale quick-select label after Reset Filters

Move selectedTimeInterval back to parent so handleFilterReset can
reset it to the 24-hour default. The toolbar receives it as a prop.

* refactor useLogFilterLogic tests for controlled-hook + backend-query shape

The hook no longer owns filter state or does client-side filtering — it
receives filters/setFilters as props and drives filteredLogs from a
useQuery over uiSpendLogsCall. Reshape the tests around that contract:
introduce a controlled harness that owns filter state, collapse the 10
per-filter assertions into a single it.each over filterKey → API param,
and drop the client-side passthrough tests (the .min test file and the
"return all logs when no filters" / "empty when logs null" cases) that
no longer correspond to any hook behavior.

* cover new useLogFilterLogic invariants: activeTab gate, filterByCurrentUser fallback, debounce negative, partial merge

Follow-up to the test refactor. Adds coverage for invariants the
refactored hook contract introduced but that the first pass didn't
assert:

- query enablement: expand the single accessToken-null case into an
  it.each over all four credential props (accessToken, token, userRole,
  userID), plus a separate test for activeTab !== "request logs"
- filterByCurrentUser: when true with a blank User ID filter, the
  outbound request carries user_id = userID
- debounce: also assert the negative case — no call in the first 100ms
  after a filter change (first waiting out the initial mount fire)
- handleFilterChange: partial updates merge without clobbering other
  filter keys (protects the spread + default-fill semantics)
- handleFilterReset: calls setCurrentPage(1) alongside restoring
  filters

* fix typo dropping the live-tail banner border

Tailwind silently ignores unknown classes, so border-greem-200 was
leaving the auto-refresh banner with only its bg-green-50 fill and no
outline.

* memoize columns and derived table data in SpendLogsTable

The table's columns array, four-pass data pipeline, and sort-change
handler were all being rebuilt on every parent render. That made every
filter click re-instance all 23 TanStack-Table columns, re-run
filter/reduce/map over all rows, and recreate per-row click closures —
all before the intentional 300ms debounce timer even got a chance to
fire.

Local measurement (40 rows, dev mode):

    filter click → query fires: 1957ms → 1217ms (−38%)

Wrap createColumns in useMemo keyed on sortBy/sortOrder, hoist
onSortChange into a useCallback, and move the searchedLogs /
sessionComposition / sessionRepresentativeMap / filteredData derivations
into a single useMemo keyed on filteredLogs.data + searchTerm.

These were pre-existing issues on main — not regressions from the
hook refactor — but the refactor made them user-visible because the
new query debounce put render cost on the critical path.

* apply dropdown filters instantly, debounce only text inputs

Dropdown selects now bypass the 300ms debounce so a click updates the
table immediately. Text inputs (Key Hash, Error Message, Request ID,
User ID) still debounce. handleFilterReset also clears the pending
debounced value so a half-typed text filter can't re-fire after reset.

* fix(ui/spend-logs): restore lost loading/debounce behavior + cover dropped tests

Regressions from the spend-logs-view refactor:
- debounce the 'Public model / search tool' text filter (was firing a
  backend query per keystroke) via TEXT_FILTER_KEYS
- restore Fetch-button smoothing through table repaint using
  useDeferredValue on the rendered data (explicit staleness)
- show AntDLoadingSpinner during the auth-resolve phase instead of a
  blank screen on first load
- only live-tail-poll while the tab is visible
  (refetchIntervalInBackground: false)
- extract getLiveTailRefetchInterval helper for the poll decision

Tests:
- LogDetailContent: retries display (>0 / 0 / absent), overhead-absent
- log_filter_logic: regression guard that the public-model filter
  debounces; getLiveTailRefetchInterval unit tests
- logs_utils: getTimeRangeDisplay quick-select window labels

* test(ui/spend-logs): cover the cold-load auth-not-ready spinner guard

Asserts SpendLogsTable shows a loading spinner (not a blank screen)
while credentials are unresolved, and renders the table once present.
2026-05-19 10:58:48 -07:00
user
e96d850b84 chore(deps): address dependency review notes 2026-05-04 12:09:04 -07:00
user
bfdd786962 chore(deps): refresh dependency locks 2026-05-04 11:36:18 -07:00
yuneng-jiang
f3c6915d61 feat: add useChatHistory hook with tests (extracted from ChatUI)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 18:09:20 -07:00
yuneng-jiang
0cd4a68157 [Fix] Add missing networking mocks to CreateKeyPage test
The test's partial vi.mock of @/components/networking was missing the daily
activity call exports now imported by EntityUsage via ENTITY_FETCH_FNS.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 09:47:56 -07:00
yuneng-jiang
34769fdbf4 Merge remote-tracking branch 'origin' into feature/vkey-modal-squashed 2026-03-06 16:51:21 -08:00
yuneng-jiang
2285dc78b9 [Fix] UI - resolve flaky tests from leaked @tremor/react Tooltip timer
Local vi.mock("@tremor/react") overrides in router_settings tests were
clobbering the global setupTests.ts mock, re-introducing the real Tooltip
component which schedules a setTimeout. When jsdom tears down after each
test file, the pending timer fires and hits window is not defined, which
Vitest flags as an unhandled error that can cause false positive failures
in subsequent tests (including the create_mcp_server timeout in CI).

Fix: add Switch to the global @tremor/react mock in setupTests.ts (the
only reason the local overrides existed), then remove the three local
vi.mock("@tremor/react") blocks so all test files inherit the global mock
with properly stubbed Button, Tooltip, and Switch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 10:28:27 -08:00
Dibyo Mukherjee
518cd3ef60 feat(ui): add key creation deep-links with SSO return URL support
Enables deep-linking directly to the key creation modal with prefilled
form data via URL parameters, including support for preserving these
deep-links through SSO authentication flows.

Key Creation Deep-links:
- Auto-open key creation modal via ?create=true parameter
- Prefill form fields from URL parameters (team_id, key_alias, models, etc.)
- Role-based access control for auto-open (requires write access)
- Race condition protection for redirect handling

Example: /ui?create=true&team_id=abc&key_alias=my-key&models=gpt-4,claude-3

SSO Return URL Preservation:
- Cookie-based return URL storage (works across ports for SSO flows)
- URL validation to prevent open redirect attacks
- Support for both dev and production environments

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-26 15:41:54 -05:00
yuneng-jiang
825503fa1b Refactor react-query hooks 2025-12-24 10:13:55 -08:00
yuneng-jiang
dc3bdffaee Migrate MCP servers to react query 2025-12-22 14:33:38 -08:00
yuneng-jiang
0635b1cbf0 rename 2025-12-11 16:33:23 -08:00
yuneng-jiang
52cb54968a Change to useAuthorized hook 2025-12-11 16:28:03 -08:00
yuneng-jiang
c1c8a6937e Renaming + fixing tests 2025-12-11 16:11:09 -08:00
yuneng-jiang
424934296f adding all files 2025-12-11 15:40:26 -08:00
yuneng-jiang
816854e426
Merge pull request #17623 from BerriAI/litellm_usage_page_ui_improvements
[Feature] Logs Spend Enhancements
2025-12-06 19:56:49 -08:00
yuneng-jiang
4e4a2e7ca2 Spend logs spend column enhancement 2025-12-06 16:14:33 -08:00
yuneng-jiang
805a8f0d9f Loading states for Edit Membership modal 2025-12-06 15:27:47 -08:00
yuneng-jiang
5496e622c1 Unit tests 2025-12-03 23:46:09 -08:00
Ishaan Jaffer
c6b8f19adc ui unit tests fix 2025-11-22 14:05:59 -08:00