* fix(ui): highlight the first member search match so Enter picks it
Moving the modal off antd Form swapped antd Select for Base UI Combobox, which
highlights nothing until an arrow key moves the cursor. Typing an email and
pressing Enter therefore selected no one, and the form submitted anyway, so
/team/member_add went out with member.user_email undefined.
autoHighlight="always" restores the behaviour antd had. The Enter key still
does not submit the form, which the existing test continues to cover.
* fix(ui): keep the always-highlight prop past the type check
Combobox.Root re-declares autoHighlight as boolean while the AriaCombobox it
wraps types it as boolean | "always". Only "always" highlights a list this
component filters server-side, since the plain flag highlights on Base UI's own
filtering pass, which filter={null} turns off.
Verified with next build, the same type check that failed in CI.
* refactor(ui): extract the create-key payload builder out of create_key_button
handleCreate built the POST /key/generate body inline by mutating the object antd
handed it, across roughly 170 lines mixing form values, twelve pieces of React
state and five sources that all fold into object_permission. Nothing could assert
that shape without rendering the whole modal.
The construction now lives in createKeyPayload.ts as a pure function returning a
tagged union, with 46 unit tests that run in single digit milliseconds and pin
whole payloads with toStrictEqual, following the createServerPayload pattern the
dashboard CLAUDE.md documents.
Behaviour is unchanged. The extraction was checked against the pre-extraction
handler with a differential harness over 50 input combinations, comparing the
built object, its key order and its serialised bytes, and eight mutations of the
new module were each confirmed to fail the committed tests.
* refactor(ui): align the key payload builder to the programme's entry-condition spec
Folds the duplicate-alias guard and the endpoint choice into the builder, so
KeyPayloadResult now carries three variants and handleCreate holds no payload
decision of its own. The two failure branches keep their original position
around toast.info and setIsModalVisible, and the builder checks the alias before
the agent selection, so the observable order is unchanged.
Pins the serialised wire shape as well as the object. The closed form registers
team_id at null through initialValue while organization_id has no initialValue
and stays undefined, so an untouched create sends seven of its eight keys.
Opening Optional Settings takes the object to 23 keys and the wire to nine. Both
directions of the definedness contract are now covered: undefined must not
become null, and null must not be dropped.
The earlier fixture fed a team_id the closed form cannot produce and pinned a
six-key wire as a result.
* refactor(ui): fold the service-account metadata write into its only caller
Removes assignServiceAccountId as a separate helper so there is no mutating
function available for reuse, which was the substance of the review finding. The
write now sits two lines below the JSON.parse that produced the value, so it is
visibly local and cannot reach a caller-owned object.
The write itself stays. Metadata has no validation rules, so a user can submit a
JSON body that parses to a primitive or an array. On a primitive the property
write raises a TypeError and the existing catch surfaces an error toast, and on
an array it leaves the array intact through JSON.stringify. A spread coerces
both to plain objects instead, silently creating a key from input the form
rejects today. Two tests pin those cases and go red against the spread.
* test(ui): repoint the e2e locators at the post-antd form controls
Nine Playwright specs went red after the tremor and antd removals, none of
them because the product broke. The specs selected on markup those libraries
owned: tremor's TextInput stamped data-testid="base-input", the antd toast
facade rendered .ant-notification, and the team member modal's email field was
an .ant-select. Removing the libraries deleted those hooks silently.
Repoint each onto a user-facing locator that survives the next migration:
getByLabel for the key name, the MCP tool argument and the two cache pricing
fields, getByRole("combobox") for the team member email search, and the toast
container for the add-model success message.
The pricing fields needed a source change to be reachable at all. antd's
Form.Item used to assign the field name as the input id and tie the label to
it; the react-hook-form rewrite renders FieldLabel with no htmlFor and lets
FormField generate an opaque control id, so both cache inputs lost their
accessible name and could only be told apart by placeholder, which they share.
Pin the id back to the field name and point the label at it.
* fix(ui): let FormField own the pricing field label instead of hand-rendering one
The previous commit gave the cache cost inputs an accessible name by rendering
a FieldLabel with htmlFor next to the FormField. FieldLabel forwards Label's
props, and Label only accepts children, so next build failed type checking.
FormField already renders a label wired to the control id it generates, and
FormField.test.tsx covers that association, so passing label through is both
type-safe and less markup. The read-only branch keeps its plain FieldLabel,
which has no control to point at.
* refactor(ui): migrate the key edit form off Ant Design onto react-hook-form
The key editor was the last antd Form in the key flow. antd's store decided
the saved payload implicitly: onFinish reported whatever mounted Form.Items
happened to be registered, so a control could stop feeding the request
without anything failing.
The form now runs on react-hook-form with the dashboard's own field
primitives, and the payload is projected explicitly in keyEditFormValues so
every saved key is written out by name. That reproduces the old payload
exactly, including the keys antd sent holding undefined and the two
role-gated keys it dropped entirely when the field was not mounted.
The existing suite is kept as the contract and passes unedited apart from
the selects, whose queries moved from Ant Design class selectors to roles
and labels. MultiSelect now forwards a per-option disabled flag, which the
Models select needs to keep greying out individual models once the
all-proxy-models sentinel is picked.
* fix(ui): keep the key edit prompts control gated behind premium
The antd control carried disabled={!premiumUser} and a tooltip saying
prompts by key are premium. The port kept the premium placeholder but
dropped the gate, so a non-premium admin could type a prompt and have the
whole save rejected by the endpoint.
TagsInput had no disabled prop at all, which is why the gate could not
survive the port; it now takes one and passes it to the combobox. An audit
of every disabled expression against the antd original shows this was the
only gate lost.
Also pins the two payload keys that are assembled in the submit handler
from React state rather than bound fields, budget_fallbacks and
tag_rpm_limit. A field-driven suite cannot see them, and tag_rpm_limit's
only previous appearance was an empty map that reads the same whether the
assignment works or is deleted. The cases were written against the antd
implementation by another lane and are added unchanged.
The create key suite mocked antd wholesale and swapped in a fake form
store whose onFinish spread every value it had ever seen, which is the
inverse of what rc-field-form does: real antd reports only the mounted
registered fields, so anything inside a closed collapsible is absent
from the body rather than present and null. Fourteen children were
stubbed to () => null on top of that, so the suite passed no matter
what the form would actually submit and could not protect a port off
antd.
The integration test now renders the real tree and stubs only the
network boundary, and a submit payload contract block pins the exact
body with toStrictEqual so null, undefined and absent stay distinct.
It covers all sections closed, Optional Settings alone, all open, and
each nested section opened by itself, plus the collapse and re-expand
path that antd's store survives and a shouldUnregister port would not.
The genuinely unit-level logic, fetchTeamModels and fetchUserModels,
moves to a plain unit test that mocks modelAvailableCall alone.
getOpenAPISchema stays reachable through a stubbed global fetch rather
than the networking module mock, because networking imports jsonFields
from check_openapi_schema and that module imports getOpenAPISchema back,
so importOriginal binds the real export through the cycle.
`mcp_server_edit` builds its update payload from `validateFields()`, so the
request body is whatever antd had mounted at the moment Save was pressed.
Nothing asserted that body, and the existing suite has no `toStrictEqual`
anywhere, so a key appearing, disappearing, or arriving as null instead of
undefined was invisible.
This adds a characterization net that drives the real component tree and
captures the exact object handed to `updateMCPServer`. Every case is pinned
with `toStrictEqual`, which is the only matcher that separates the three
states a key can be in: absent, present-as-undefined, and present-with-value.
That distinction is the whole point here, since a gated-off field and a
mounted-but-empty field currently differ, and `entra_obo` versus plain
`token_exchange` is exactly that difference in the wild.
19 cases cover the transports, all ten auth types, the credential subsets each
one contributes, and the tool allowlist gate. Values were captured from the
component as it behaves today rather than written from reading the source, so
the file records current behaviour instead of intended behaviour
A six-mutant battery run against the payload builder kills 6/6, each run
executing all 19 tests: leaking an audit field, deleting a binding, removing a
gate, turning a flat key into a lodash path, swapping undefined for null, and
swapping an empty-array default for null
Two explicit assignments in the payload turn out to be dead. `alias` and
`mcp_access_groups` are both already supplied by the `...restValues` spread
above them, so removing either line changes nothing. Left in place here and
noted for the payload extraction that follows
* refactor(ui): codemod the antd Tooltips outside form files onto the shadcn atom
The atoms/Tooltip wrapper gains an optional side and renders its children without a popup when there is no content, which covers antd's placement prop and its title={undefined} escape hatch. A TypeScript-AST codemod then rewrites every antd Tooltip that does not sit in a file still using antd Form, since those files get their primitives swapped as part of the react-hook-form migration.
* refactor(ui): fold the atoms Tooltip into the ui tooltip primitive as SimpleTooltip
team/LoggingSettings.tsx carried 34 hardcoded palette classes and
common_components/PremiumLoggingSettings.tsx another 9, so both render
light-only regardless of theme. Map the neutrals onto foreground,
muted-foreground, muted and border, the red affordances onto destructive,
and swap the hand-rolled chips for the shadcn Badge primitive.
The three event-type options carried decorative green, red and blue dots.
The design system has no success or info token, so the dots are dropped
and the option labels, which already say "Success Only", "Failure Only"
and "Success & Failure", carry the meaning on their own.
This is groundwork, not a visible change: nothing in the dashboard ever
applies the .dark class today, so the dark palette is unreachable. The
files no longer hardcode colour and will follow the theme once one exists.
* test(ui): characterize the model info and cache control submit payloads
Pins the antd behaviour these forms have today, ahead of moving them onto
react-hook-form: the full model info PATCH body, the sticky touched-field
semantics that decide which pricing keys ship, the mounted-only cache control
keys, and the string-typed injection point index.
* refactor(ui): move the model info edit form off antd Form
The deployment edit form on the model info view now runs on react-hook-form
with a zod resolver and shadcn controls, extracted into ModelInfoEditForm so
the view keeps the payload builder and the form keeps the fields.
Cache control injection points become a presentational value/onChange child,
which lets the model info view host it through react-hook-form while the add
model form keeps hosting it through antd. That child never wrote to a real
store on either side: it registered under cache_control_points while both
parents read cache_control_injection_points, so its form prop was inert.
antd marks a field touched on change and never clears it, and neither
touchedFields nor dirtyFields reproduces that, so the four pricing keys that
gate on it track first change explicitly.
The PTU rules move from antd validator wrappers to pure predicates that both
surfaces share, since the add model form still feeds the wrappers to its own
antd form.
* refactor(ui): trim comments and type the model record prop on the edit form
Cuts the explanatory comments that the house rules do not allow, keeping
only the three that record non-obvious library behaviour plus the eslint
directive, and narrows the modelData prop to the two fields the form reads.
Corrects the claim in 5b7ecede4e that the cache control child registered
the wrong key. At the staging tip the Form.List registration is on
cache_control_injection_points and is live, which is why this PR rehosts
it into advanced_settings. The dead part is the three
getFieldValue("cache_control_points") readers, whose key nothing
registers, so updateCacheControlPoints dereferences undefined and the
caught error reaches the console on every role, index or remove change.
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.
The dashboard had no shared tokens for non-destructive status colours, so
components reached for raw Tailwind shades instead. Add --success, --warning
and --info alongside the existing --destructive, in both :root and .dark, and
register them in @theme inline so the usual utilities resolve.
Light values are picked for legibility as foreground text rather than by
copying a fixed shade number. Tailwind's ramps are not perceptually aligned
across hues, so amber-600 and green-600 sit at 66.6% and 62.7% lightness and
fail WCAG AA on white (3.19:1 and 3.22:1). green-700, amber-700 and blue-600
land at 52.7%, 55.5% and 54.6%, the same band as --destructive at 57.7%, and
clear AA. Dark mode uses the -400 shades, matching --destructive.
The .dark values are populated even though nothing can apply that class yet.
They are the artifact the later theme switch work will turn on.
Alert moves its info and warning variants onto the tokens. The tint is /5
rather than /10 because /10 drops both below AA. The error variant keeps its
existing shades: it involves no new token, and its current 9.21:1 is better
than anything the token form would give it.
* refactor(ui): move the agent, guardrail, prompt, policy and skill forms off tremor
The agent info Save Changes button used to rely on tremor's implicit
submit inside the antd Form, so it now carries an explicit type="submit".
Every converted TabsContent is keepMounted to keep tremor's always-mounted
panel semantics, pinned by a new guardrail info test. Prunes the tremor
no-restricted-imports suppressions these nine files no longer need.
* fix(ui): keep the line tab strip on the agent, guardrail and prompt info views
tremor's TabList defaulted to the line variant while shadcn's TabsList
defaults to the filled pill, so the bare conversion turned three underlined
tab strips into segmented pills. Restores the line variant plus the bottom
border and the tab padding the strips used to have.
* test(ui): pin the agent settings submit and the prompt raw json tab
Agent Settings only saves because Save Changes carries an explicit
type="submit" now that the button is a Base UI button, so a test drives the
edit and asserts the patch call fires. The prompt info tabs are keyed by slug
now, which also makes Raw JSON render for prompts with no template, so a
second test renders that case and asserts the serialized response is visible.
* 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.
The antd form reset on [form, actualSchema, tool], so it reseeded whenever
the schema changed and not only when a different tool was picked. Keying the
migrated form's remount on tool.name alone narrowed that: a same-named tool
whose schema changed would have kept its old indexed values and submitted
them under the new schema's keys, and any field the new schema added would
never get its default. The key now covers the schema content as well.
Reachability, so nobody reads more into this than is there: selectedTool is a
state snapshot set on click, so this is not reachable through the current
parent. It is a latent divergence rather than a live bug, and it is fixed
because the contract for this migration is zero functional change.
The schema default helpers move into the pure module beside the other
argument logic. They arrived carrying the original's explicit any and built
their result by mutating it; they are now typed with unknown, built by
spread, and covered directly by unit tests rather than only through a render.
Also takes the last five hardcoded neutrals onto tokens. The earlier pass
scanned the gray family only and did not see the slate ones in the tool name
chip, which had no dark variant. Counting colour utilities that have no
dark: counterpart reads 0 for this file now, against 64 before the migration.
The Base UI combobox only renders a selected value that is present in its
item list, so the port synthesizes an item for the current value when the
search results no longer contain it. That synthetic item carried an empty
user, and selecting it ran the same handler as a real result, wiping both
the email and the user id before submit.
The synthetic item now carries no user at all and the select handler
ignores it, so reselecting the value already in the field leaves both
identity fields alone. antd needed none of this: its Select renders a
value that is absent from its options.
The team create and edit forms send a different set of keys depending on
which collapsible sections the user opened, because a closed section is
unmounted and its values never reach the request. Nothing covered that,
so a form rewrite could change the request body without failing a test.
Pins the exact key set the create form sends with every section closed,
the keys Additional Settings adds once opened, and that a value typed
then re-hidden is dropped while a reopened one is restored. Does the same
for the team member and search tool sections on the edit form, asserting
absence at the wire level rather than just comparing values.
Also hardens two option queries in the member modal suite onto the option
role, and lifts the duplicated mock seeding in the team info suite into
one function both blocks call.
* test(ui): raise vitest test and hook timeouts for CI headroom
The UI unit suite runs about 3x slower on the CI runner than locally, which
put the slowest cases right on the 30s per-test limit. TeamInfo's pass
through routes case takes ~8s locally and has been failing on staging at the
timeout across consecutive runs even though it passes reliably when run
directly.
Raise testTimeout to 60s and set hookTimeout to 30s so the current slow cases
have headroom. This is a stopgap while the suite gets split into proper tiers,
not a fix for the underlying per-test cost.
* test(ui): query agent form panels with findByRole like the rest of the file
The panel helper was the only synchronous query in add_agent_form's
integration test; every other lookup already retries via findBy. On the CI
runner the second case has been failing with "Unable to find an accessible
element with the role button and name /Cost Configuration/" against a modal
whose body had not rendered.
Make the helper retry like its siblings and await it at each call site.
The tool test panel drove its argument fields through an antd Form, so the
call payload was whatever rc-field-form happened to have mounted. It now runs
on react-hook-form with shadcn controls, and the payload itself lives in
toolCallArguments.ts as a pure function of the schema fields plus the entered
values.
Fields bind by index rather than by name, because an MCP tool's JSON schema
can name a property anything: a key containing a dot would be one flat key to
antd but a nested path to react-hook-form. Binding to args.0, args.1 and
zipping back to the real keys at submit time keeps the emitted arguments
identical whatever the server calls its properties.
Coercion, the blank filter, the required and JSON rules, and the params
wrapper for nested-object schemas all keep their previous behaviour, and the
neutral colours in the panel move onto tokens so it reads correctly in dark
mode.
* refactor(ui): move the admin, SSO, SCIM, alerting and fallback forms off tremor
Swaps the tremor Button, Card, Callout, Grid, Divider, Text, Title, TextInput, Table parts, Badge, Icon and Switch in these nine files for the shadcn layer and lucide icons, keeping antd in place. The SCIM create-token button keeps an explicit type="submit"; the two SCIM copy buttons sit outside the antd form so they stay plain buttons, and the alerting form's Enterprise Feature upsell button is a link wrapper rather than a save action, so it deliberately stays type="button" while the form keeps its own Update Settings submit. The teal login callout on the admin panel maps to the info Alert variant since no teal variant exists. Prunes the nine tremor no-restricted-imports suppressions these files no longer need.
* fix(ui): keep the alerting settings name column left aligned
tremor's TableCell hardcoded text-left, so the align="center" attribute never took effect. The shadcn cell has no text-align of its own, so translating that attribute into text-center would have centered the field name and its description for the first time.
* fix(ui): restore the CloudZero key reveal toggle and the SCIM divider gap
tremor's TextInput drew its own show/hide button whenever the type was
password and the field was not disabled, so the straight pass-through to
the plain shadcn Input silently deleted that affordance from the CloudZero
API key field. Rebuilds it with the InputGroup reveal pattern that
email_settings.tsx already uses, behind a small local control so the antd
Form.Item keeps its id, value and onChange wiring and the field still
matches its shadcn sibling in the same form.
Also puts the SCIM separator back on my-6: tremor's Divider was
"w-full mx-auto my-6", and the conversion shipped my-4, tightening that
gap by 8px on each side. The disabled SCIM token field stays a bare Input
because tremor suppressed its toggle when disabled too.
The selector takes value and onChange, which is exactly what lets it sit
inside both an antd Form.Item and a react-hook-form FormField, so the doc
bullet naming only antd was about to describe half the truth. The props
interface already states the contract.
That bullet was also the only Form.Item match in the file, and it has twice
inflated the migration's canonical tag count, which now needs no subtraction.
Its four hardcoded colours move to tokens for the same reason as the rest of
the sweep. Its antd Select stays.
The agent forms moved to react-hook-form in #37357, which deliberately kept
the antd Select for the agent type picker because its dropdownRender footer
and two-tier options have no shadcn equivalent. The Form.Item wrapped around
it survived as a side effect rather than for that reason: it carries no name
and no rules, and the file renders no antd Form at all, so it bound nothing
and validated nothing.
It was also the only label in the file rendering antd's required asterisk,
while AgentFormField renders genuinely required fields without one. Moving it
to the file's own Field, FieldLabel and labelWithHint makes it match, and
gives the label a control to point at.
The shared alias editor was light-only: 18 hardcoded palette classes across
its headings, table cells, config preview and five raw controls, so it read
grey-on-grey against the dark dashboard theme.
Its five buttons also carried no type attribute, and all three consumers
render it inside a real antd Form with an onFinish. Measured against the
pre-change file in jsdom with a liveness gate on both sides: clicking Add
Alias fired the parent's onFinish once, and a row action fired it once more.
So editing a model alias inside the create-team, team-update or create-key
form also submitted that form. shadcn Button renders type="button", which
closes the path.
Swapping the raw input and button elements for the shadcn primitives is what
makes the colours resolve, since a bare element needs the whole token set
hand-written to work in both themes.
`items` feeds `hasNullItemLabel` as well as label resolution, and that
selector decides whether `Select.Value` renders the placeholder. None of
the twenty fixed sites has a null-valued item, so their placeholders are
unchanged, but nothing pinned that.
Cover both halves: the placeholder still renders when nothing is
selected, and a null-valued entry in `items` takes over from it. The
second case is the trap to avoid when adding `items` to a Select that
relies on its placeholder.
Base UI's Select.Value resolves an option's label only when the root
carries an `items` prop or the Value has a child. `resolveSelectedLabel`
in @base-ui/react/internals/resolveValueLabel.js falls through every
branch to `stringifyAsLabel(value)` otherwise, and `state.items` is
written only from the root's `items` prop, so the `<SelectItem>` children
rendered inside `<SelectContent>` never populate it.
A self-closing `<SelectValue />` on a root without `items` therefore
renders the raw value once something is selected. The placeholder branch
still works, so the trigger looked right until the user picked an option
and then showed `development` for Development, `LiteLLM_VerificationToken`
for Keys, `all` for All Actions, and `24h` for Daily.
Pass `items` at the 20 affected sites, using the array form the other 52
call sites already use. Where a literal option sat alongside mapped ones,
build one array and map the options over it so the labels and `items`
cannot drift.
The record-map form is avoided deliberately: `items[value]` on an object
literal reaches Object.prototype, so a dynamic value named `toString`
would resolve to a function and React would throw on it. The array form
matches with `.find` and has no prototype lookup, which matters where the
values are user-supplied model groups, team ids and key aliases.
Also replace the option lookup in CompetitorIntentConfiguration's test
helper, which searched by text and clicked the last match. That match is
now ambiguous because the trigger carries the label too, and the helper
already flaked roughly one run in six before this change.
Ports user_search_modal from antd Form to react-hook-form plus the shadcn
kit, keeping the antd Modal and Alert shells. Payload parity was proven by
rendering the antd original beside the migration in one describe.each: an
untouched submit yields the same three keys with the identity fields
undefined, and picking an option yields the same email and id on both sides.
antd Select swallows Enter, so the original never submitted from a field.
The Base UI combobox does not, which added an Enter-to-submit path; the
inputs now swallow Enter and both sides measure zero submits from every
field with one from the button.
* refactor(ui): move the budget, cache, cost tracking and playground forms off tremor
Swaps tremor Accordion for the Base UI Collapsible, TextInput for the shadcn Input and the two
tremor Buttons for the shadcn Button across the budget modals, cache settings, the cost tracking
add-provider and add-margin forms and the playground model selector. The accordion bodies keep
tremor's unmount-when-closed semantics, since headless-ui's Disclosure.Panel and Base UI's panel
both default to unmounting, so the antd fields inside behave exactly as before.
The two cost tracking buttons are the one deliberate behaviour change. tremor's Button renders a
bare button with no type, so inside the antd Form that wraps both components it was an implicit
submit on top of its own onClick. For the discount form that meant every click ran
handleAddProvider twice, once from onClick and once from the form's onFinish, and for the margin
form the submit did nothing at all because that Form has no onFinish. The shadcn Button forces
type="button", so the add now fires once from onClick alone and no type="submit" is added back.
The three inputs that used onValueChange now read e.target.value, and each one gained a test that
types into it and asserts the reported string, so the wiring cannot silently regress. The cache
settings suite gained a collapse contract test that the advanced sections are absent until the
section is expanded. Prunes the six no-restricted-imports suppressions these files no longer need,
each dropping from two to one for the antd import that stays.
* fix(ui): keep enter to submit on the cost tracking add forms
The shadcn Button forces type="button", so converting the two tremor buttons left both cost
tracking modals with no submit button at all. Each form still holds two fields that block
implicit submission, the provider select's search input and the value input, so pressing Enter
stopped adding anything. Both buttons get type="submit" back.
For the discount modal that alone would restore the double add the conversion had just removed,
since a submit also ran the form's onFinish, so the parent drops onFinish and the now dead
handleFormSubmit. Click and Enter both go through onClick exactly once. The margin form's parent
never had an onFinish, so restoring the submit type there is enough on its own.
Adds three cases to the cost tracking settings suite: the discount add fires once from a click,
the discount add fires once from Enter, and the margin add fires once from Enter. Dropping either
type="submit" kills the Enter cases and putting onFinish back makes both discount cases see two
calls. Also drops the two empty placeholders on the budget modals that only existed to suppress
tremor's "Type..." default.
* fix(ui): restore Enter-to-submit on the margin modal
The tremor Button rendered a bare native button, which defaults to
type="submit", so Enter in the percentage field submitted the margin
modal. The shadcn Button wraps Base UI, which defaults to type="button",
and the migration also replaced the margin modal's form element with a
plain div, so Enter went inert while the visually identical discount
modal kept working.
Give the margin modal the same form wrapper the discount modal already
has and mark its action button as the submit button. Also move the cache
settings advanced-section test into the integration file, where a test
that renders the real component tree belongs.
* refactor(ui): move the teams page and team detail views off tremor
Swaps the tremor Accordion, Badge, Button, Card, Grid, Text, TextInput and
Title usages in Teams.tsx, TeamInfo.tsx, EditMembership.tsx and
LoggingSettings.tsx for the shadcn layer. The team model badge colour map
becomes a variant map: all-proxy, direct and access-group chips render as
secondary and no-default as outline, so the kind is now conveyed by the
tooltip rather than by hue. The LoggingSettings top decoration is drawn with
border-t-4 border-t-blue-500 and its light red Remove button becomes a ghost
button with red text. antd stays in place for this pass and the eslint
no-restricted-imports counts for the four files ratchet down by one each.
* fix(ui): keep the password reveal and model badge hues in the team views
The tremor TextInput rendered a show/hide button for every password field, so
the shadcn swap silently dropped it for the sensitive logging parameters. The
password branch now renders an InputGroup with an eye toggle, matching the
pattern email settings already uses, and a test pins the masking.
The team model chips go back to four distinct colours by way of the shared
StatusBadge, so a directly granted model still reads differently from an
access group one without hovering for the tooltip.
The hand-drawn blue accent on the logging integration card is dropped: the
tremor decoration it replaced never rendered, because the caller's own border
classes won the class merge, so the bar was new rather than preserved.
* fix(ui): drop the dead empty placeholder on the team name field
The team name input carried placeholder="" only to suppress tremor
TextInput's default "Type..." hint. shadcn Input has no default
placeholder, so the empty string does nothing and the field now relies on
its label, matching the other converted create-team fields.
Removes one named callback from a team and leaves the team's other callbacks
registered and firing. Before this, the only removal route was
POST /team/{team_id}/disable_logging, which clears every callback at once, so a
tenant sharing a team could not deregister its own integration
The handler filters metadata["logging"], keeps the survivors encrypted, refreshes
the cached team so the removal applies to keys that are already live, and emits a
redacted audit row, matching what the add and disable routes do
Resolves LIT-5161
* refactor(ui): move the add model and credential forms off tremor
TextInput becomes the shadcn Input, Text becomes a sized paragraph, the
advanced settings Accordion becomes a bordered Collapsible, and the
Team-BYOK Switch moves to the Base UI switch with onCheckedChange plus an
aria-label. That switch stays wrapped in a span so the antd Tooltip still
shows on hover while it is disabled for non-premium users, matching what
the tremor wrapper div did. provider_specific_fields keeps antd's
Input.TextArea through an AntdInput alias so its antd import stays a single
statement. Prunes the tremor no-restricted-imports suppressions these files
no longer need.
* fix(ui): keep the reveal toggle on the provider secret fields
tremor's TextInput drew its own show/hide button whenever the type was
password, and the shadcn Input is a plain native input, so every provider
secret this form renders (API keys, client secrets, and the same fields
inside the add credential modal) lost that affordance.
Puts the password branch on antd's Input.Password, which is what the other
dynamic credential forms in the dashboard already use, so the reveal comes
back and the antd Form.Item wiring stays untouched. The control chain moves
into an early-return helper, which keeps the extra branch from pushing the
file past its no-nested-ternary budget and drops that count from 5 to 3.
* refactor(ui): move the virtual key create and edit forms off tremor
Swaps the tremor primitives in the create-key modal, the key edit view and
their two shared field components for the in-repo shadcn layer: Accordion
becomes Collapsible, Grid/Col become grid divs, Text/Title become real
paragraphs and headings, and TextInput becomes the shadcn Input. antd stays
where it already was, so the antd Input keeps rendering the textareas and
hidden fields under the AntdInput alias.
Two behavioural notes. The key edit view's Save Changes button keeps saving
because it carries an explicit type="submit"; Base UI's button otherwise
defaults to type="button". Its Cancel button now really is type="button",
where the tremor one had no type at all and so submitted the form on top of
calling onCancel, and a test pins that.
Base UI's Collapsible panel unmounts while closed exactly like the headless
Disclosure panel tremor wrapped, so the create-key tests now open Optional
Settings before querying inside it instead of relying on a tremor mock that
flattened every accordion.
* refactor(ui): hoist the create-key collapsible header classes and keep optional settings a heading
Names the repeated Collapsible trigger and chevron class strings the way the
cost tracking conversion does, since nine copies of each lived in this one
file, wraps the Optional Settings trigger in an h3 so the section keeps a real
heading next to Key Ownership and Key Details, and drops the placeholder=""
that only ever existed to suppress tremor's default hint.
* feat(vector_stores): add Valkey as a managed vector store provider
Adds a valkey provider for managed vector stores, searchable via the
valkey-search module over RESP. Introduces BaseDirectVectorStoreConfig
for datastores that execute searches directly instead of building an
HTTP request, and refactors the valkey semantic cache to share the new
connection URL helper. Registered in the provider enum, router params,
proxy config registry, Admin UI Add Vector Store modal, and provider
endpoint support matrix.
* fix(vector_stores): join list queries and bound valkey socket timeouts
Review feedback: multi-string queries are now space-joined like every
other embedding-based provider instead of dropping all but the first,
and the request timeout is threaded through the direct vector store
interface into bounded socket_connect_timeout / socket_timeout values
on both redis clients so an unreachable Valkey host cannot pin proxy
workers until the OS TCP timeout.
* chore(ui): regenerate schema.d.ts for valkey vector store fields
* docs(ui): make the Valkey vector store setup note and field tooltips explicit
* feat(ui): pick the Valkey embedding model from the proxy's models like Milvus
* fix(ui): number the setup steps in the vector store provider alerts
An internal user who administers an organization saw an empty
Organization Usage dashboard and had to be promoted to proxy admin to
see any of it.
Two independent gates were closed on them. The route layer rejected
GET /organization/daily/activity with 401 before the handler ran, since
the route belonged to no list a non-proxy-admin can reach, and the
handler's own org-admin scoping was therefore dead code. In the
dashboard, viewOrganizationUsage was granted by session role alone, and
an org admin's session role is internal_user, so the Organization Usage
option never rendered and its data fetch stayed disabled.
The route now sits in self_managed_routes, where the handler restricts
results to organizations the caller is ORG_ADMIN of and 403s on any
other org, and viewOrganizationUsage joins the existing per-capability
org-admin allowance that already covers viewDeletedTeams.
A caller who administers no organization resolves to an empty id list
rather than to None, so the organization-alias lookup is scoped by that
same list instead of reading the whole table.
The Usage page falls back to the global view when org-admin membership
is revoked while it is open, so the selector never keeps a value it no
longer offers.
* refactor(ui): migrate the MCP per-user env vars modal to react-hook-form and shadcn
Moves UserEnvVarsModal off the antd Form store onto react-hook-form with a
zod schema built from the server's declared per-user variables, and swaps
antd Input.Password for the shared PasswordInput.
The submit payload is unchanged: every declared variable is still sent as a
key, trimmed, with an untouched field sending an empty string. antd reset
the store from the modal's afterOpenChange; the migrated form reproduces
that by remounting on the same callback, so reopening still starts blank.
Adds UserEnvVarsModal.test.tsx, which was written against the antd original
and proven green before any production change, then re-run unedited against
the migration. Two further cases cover the reveal toggle, which antd
provided through visibilityToggle.
* refactor(ui): migrate the MCP toolset create and edit form to react-hook-form and shadcn
Moves the toolset name and description fields off the antd Form store onto
react-hook-form with a zod schema, and takes the surrounding panel onto
semantic colour tokens so the tab renders in dark mode. The purple selected
tool styling keeps its hue and gains dark variants rather than flattening
to neutral.
Payload is unchanged: create still sends toolset_name, description and
tools, an untouched description is still the empty string rather than
undefined, and the tool selection is still held outside the form. The antd
form carried no onFinish and its buttons sit outside the form element, so
the migrated form keeps submit on the footer button and neutralises its own
submit rather than introducing Enter to save.
Adds MCPToolsetsTab.test.tsx, proven green against the antd original before
any production change and re-run unedited afterwards.
* refactor(ui): migrate the MCP tool arguments form to react-hook-form and shadcn
Moves the schema-driven tool argument form off the antd Form store onto
react-hook-form. Validation moves to an explicit resolver that reproduces
antd's rules field by field, including the per-field required message and
the JSON object and array messages, and the same resolver is reused by
getSubmitValues so the imperative path and the rendered errors cannot
disagree.
getSubmitValues still rejects with a plain object carrying errorFields
rather than an Error. ChatUI branches on `err instanceof Error` to choose
its toast, so rejecting with an Error would have silently changed the
message the user sees. That is pinned by a test proven green against the
antd original with a Form.Item liveness gate, and proven red when the
rejection is switched to an Error.
Enum and boolean fields keep the antd Select, whose allowClear has no
shadcn equivalent; dropping it would remove the only way to unset an
optional enum. Everything else moves to the shadcn Input and Textarea and
onto semantic colour tokens.
Adds MCPToolArgumentsForm.test.tsx covering the string, integer, number,
boolean, object, array, nested-params and string-schema paths, written
against the antd original and re-run unedited afterwards.
* refactor(ui): drop the decorative antd Form.Item from the MCP connect guide
The connect guide rendered a single antd Form.Item with no field name and no
Form ancestor, so it registered nothing and carried no payload; it was only
supplying bottom margin. It becomes a div with the same margin class, which
removes the file's last antd Form dependency.
Also takes the guide onto semantic colour tokens so it renders in dark mode.
The blue and green callouts keep their hue and gain dark variants rather
than flattening to neutral, since the colour carries meaning there.
* chore(ui): ratchet the MCP tool arguments form lint suppressions
The react-hook-form migration removed four of the five nested ternaries
in MCPToolArgumentsForm, so lower the grandfathered count to match and
hoist the one inline object literal the budget rule flags.
* test(ui): classify the MCP modal batteries as integration tests
Both render a real component tree down to the form controls and stub only
the network boundary, which is the repo's definition of an integration
test rather than a unit test. The tool arguments battery renders a single
module in milliseconds, so it stays unsuffixed.
* refactor(ui): migrate agent forms to react-hook-form and shadcn
Move the agent create wizard and the agent detail editor off antd Form onto
react-hook-form with shadcn primitives. The two parents share three children
(agent_form_fields, dynamic_agent_form_fields, cost_config_fields), so the whole
form graph migrates in one commit.
The submit payload is unchanged. antd validates and submits only fields that are
currently mounted, and its Collapse panels mount lazily on first open and then
stay mounted, so a payload depends on which panels the user ever expanded.
react-hook-form keeps every registered value instead, so the panels track their
own mounted set and the detail editor filters the never-opened panels back out
before building the request. shouldUnregister stays off, since it drops values
for collapsed panels rather than merely excluding them from submit.
Tags, examples and forwarded header names move from antd tags-mode selects to a
combobox in the kit. Base UI clears the combobox input on blur before the blur
handler runs, so the pending text is committed from the input-clear reason,
which is what antd did when the field lost focus.
The agent type picker stays on antd Select: its popup content is not part of the
form graph, and the Base UI popup opens a macrotask later, which the existing
unit test cannot observe.
* refactor(ui): use the shared PasswordInput in the agent forms
* refactor(ui): drop the narration comments from the agent wizard
Moves the guardrail form graph off antd Form onto react-hook-form with the
shadcn field primitives. The graph migrates atomically: add_guardrail_form and
guardrail_info own the form instances, and guardrail_provider_fields,
guardrail_optional_params and LLMJudgeFields are field groups rendered inside
them, so an antd parent could not host a react-hook-form child either way.
The submit payload is unchanged. Two characterization suites, 25 cases, pin it:
each case was written against the antd original, proven green there, and passes
unedited against the migration.
Behaviour worth calling out. Nested provider fields are keyed with ":" rather
than "." so they stay flat keys the way antd stored them, since a dotted name
is a lodash path in react-hook-form and would have started shipping a nested
object. antd InputNumber clears to null and clamps on blur where a native
number input does neither, so the judge criteria weights reproduce that. The
guardrail_info submit handler is read through a ref at validation-resolution
time, matching how antd re-read onFinish, so a submit fired by the same click
that changed state still sees that state.
Two antd behaviours are preserved rather than fixed, both worth their own
follow-up: deselecting every mode blocks Next instead of falling back to the
seeded default, and a required provider-specific field is never enforced at
create time. One is fixed and disclosed: a failed validation now names the
problem instead of rendering "[object Object]", and the guardrail name label is
associated with its control, which it was not before.
MultiSelect takes an optional id so the label can point at the control.
SkipMessageSelect was duplicated verbatim in both parents and now lives in the
shared field module.
The edit project modal had seven payload tests and none of them cleared a
required field, so rewiring its submit button to send raw form values with
validation skipped left all seven green. The validation layer was untested
on that path while looking well covered.
Adds the missing case: clearing the project name blocks the save and shows
the error. It passes on current code, so the behaviour was always correct,
and it fails under the validation-bypass mutation, so it has teeth.
* refactor(ui): migrate the SCIM and Hashicorp Vault forms to react-hook-form and shadcn
Both forms move off antd Form onto react-hook-form plus the shadcn kit, with
neutral greys on semantic tokens and coloured callouts keeping their hue behind
a dark variant, so both are dark-mode ready.
Neither file had a test, so each one gained a characterization test written
against the antd original and proven green there before any source changed. The
same files pass unedited after the migration.
Two payload details the migration has to reproduce rather than tidy up. antd
onFinish emits a mounted but never-set field as a key holding undefined, and the
vault handler turns each of those into an empty string to clear it server-side,
so every rendered vault field is seeded to an empty string rather than left
absent. And the vault form still refuses to seed or send a blank sensitive
field, so a stored secret stays write-once.
SCIM keeps its Enter-to-submit path: its footer button was a Tremor Button
carrying an explicit type=submit, which Tremor forwards, so the form could
already be submitted from the keyboard.
* refactor(ui): migrate SSO, SCIM and vault forms to react-hook-form and shadcn
Moves the SSO settings form graph, the SCIM token form and the Hashicorp
Vault config form off antd Form onto react-hook-form plus the shadcn
FormField primitives, keeping today's submit payloads byte for byte.
The SSO graph migrates atomically because an antd Form.Item parent cannot
host a react-hook-form child. BaseSSOSettingsForm now owns the shared
schema, the field components and a mounted-field picker that reproduces
what antd's onFinish actually sent: rc-field-form validates only mounted
entities, so hidden provider and mapping fields never reached the wire.
submitMountedSSOValues keeps that behaviour explicit instead of leaving it
to which fields happen to be rendered.
EditSSOSettingsModal seeds through an explicit mapper rather than
spreading the server record, so a field the form does not declare cannot
leak into an update. The vault modal keeps its two distinct behaviours for
blank inputs, clearing non-sensitive fields with an empty string and
omitting blank secrets so a stored credential survives a save.
* fix(ui): render SSO select labels and guard seeding completeness
The migrated Select triggers rendered the raw stored value rather than the
option label, so an untouched Default Role showed "internal_user" and a
chosen provider showed "okta". Base UI resolves a label only through a
Value function child, so both selects now format through the same option
list that builds their items.
Adds three characterization cases the earlier suite did not reach: an
empty required provider credential blocks the submit and names the field,
reopening the modal against a different stored config replaces every
seeded value rather than merging, and every field the provider forms can
mount survives the seeding mapper. The last one fails by name when a key
is dropped from that mapper, which is the class of defect an explicit
allowlist invites.
* test(ui): cover the edit SSO modal against its real form tree
The existing modal test stubs BaseSSOSettingsForm out, so no field ever
registers and validation passes trivially. Rewiring the Save button to
call the submit handler with raw form values, skipping both validation and
the mounted-field filter, left all 112 tests green.
Adds an integration test that renders the real modal, the real form body
and the real antd shell, stubbing only the two data hooks. Clearing a
required credential now blocks the save and names the field, and a valid
save asserts the exact payload. The bypass mutation fails both cases, and
dropping only the mounted-field filter fails the payload one.
* refactor(ui): move the search tool, tag and vector store views off tremor
Swaps the tremor Button, TextInput, Text, Title, Card, Badge, Accordion and
TabGroup usages in the search tools, tag management and vector store views for
the shadcn primitives, following the tremor conversion cookbook. Both tab panels
in the vector store info view carry keepMounted so the tester's state survives
switching to Details and back, and a test pins that contract.
tremor's Button renders a bare button element with no type, so inside the antd
Forms here the Test Connection button in the create search tool modal and the
Cancel buttons in the tag editor and the vector store form were implicit submit
buttons. The shadcn Button defaults to type="button", so they can no longer
submit, and every button that is meant to submit now carries an explicit
type="submit". Clicking Test Connection and Cancel against a live proxy on the
merge base already only ran the connection test and only cancelled, so this
closes a latent trap rather than changing what the pages do.
Decrements the seven no-restricted-imports suppression counts these files no
longer need, leaving the antd half of each entry in place for the antd pass.
* refactor(ui): use the line tab strip in the vector store detail view
The detail view's tabs kept the default pill TabsList, so it no longer matched
the underline strip tremor rendered before the swap or the one the vector store
list view already uses.
* test(ui): pin the tag and vector store form save and cancel buttons
Cancel in the tag editor used to submit the form and save the tag because the
tremor button carried no type; nothing in the suite failed if it started doing
that again. Each form now has a pair of cases: Save Changes and Create still
submit, and Cancel leaves the record alone.
* fix(ui): keep the reveal toggle on the search tool API key
tremor's TextInput drew its own show/hide button whenever the type was
password, and the shadcn Input is a plain native input, so the straight
prop pass-through silently deleted that affordance from the create search
tool form's API key field.
Puts it on antd's Input.Password instead, which is what the sibling edit
form in the same directory (SearchTools.tsx) already uses for the very
same field, so the reveal survives and the two forms behave the same.
The file already imports antd, so this adds no import and no suppression.
* test(ui): pin the antd submit payloads for the key create and edit forms
Characterization only, no source change. Both suites are green against the
current antd components, so they can gate the react-hook-form migration that
follows without being edited.
key_edit_view had no exact-payload assertion, only objectContaining, so nothing
caught a form that started sending server-only key fields. The new case asserts
the whole object.
create_key_button's existing suite runs against a hand-rolled antd fake and
stubs out KeyLifecycleSettings and RateLimitTypeFormItem, so neither the real
store nor those two controls were covered. The new file drives the real antd
form and pins the network payload instead.
* refactor(ui): move the shared key form controls off antd onto shadcn
KeyLifecycleSettings and RateLimitTypeFormItem each owned an antd Form.Item and
took the parent's FormInstance as a prop, so neither could be hosted by anything
but an antd form. That is what made the key create and edit forms one
inseparable migration unit.
Both are now presentational: they take value and onChange and let the parent own
the binding, so an antd Form.Item and a react-hook-form FormField can host them
equally. The two parents keep their antd forms for now and pass the binding down
unchanged, which is why every existing payload assertion still holds.
Controls are shadcn Select, Input, Switch and Checkbox on semantic colour
tokens, so both are dark-mode ready. The rotation notice keeps its blue hue and
gains a dark variant rather than flattening to a neutral.
The form prop the two components took was already inert: antd dispatches its own
store update before calling the child's onChange, so setFieldValue was writing a
value the store had just been given.
KeyLifecycleSettings.test.tsx keeps every assertion; only the harness moves the
duration binding up into a Form.Item, and one case disables user-event's
pointer-events check because Base UI leaves a reopened select popup inert under
jsdom, reproduced on a bare shadcn Select with none of this code involved.
* test(ui): pin the role-gated key fields and tidy the new assertions
Adds the case that proves policies and prompts leave the payload entirely for a
role that cannot see them, which a react-hook-form port would otherwise start
sending from defaultValues. Green against antd like the rest.
Also hoists the two large expected payloads into named constants and drops two
unused exports, so the lane adds no new lint-budget pressure.
* fix(ui): give the key expiry input and Never Expire checkbox separate labels
The expiry label carried htmlFor for the duration input while also wrapping
the Never Expire checkbox and its own label, so the two controls shared one
ambiguous association. Splitting the row into a plain container with a label
per control makes each name resolve to the control it describes.
Also drops the prop and test comments added in this branch, which the
repository comment policy does not allow.
* test(ui): pin the create-form expiry binding to the generate payload
create_key_button coalesces a missing or blank duration to null before it
calls keyCreateCall, so the key is present in the payload whether or not the
control is bound to the form. Every existing case stayed green with the
Form.Item removed, which left the binding uncovered.
The new case opens Key Lifecycle, types an expiry, and asserts it arrives as
that value. Proven red with the Form.Item removed and green with it restored.