From b13fabe9c26836680c7ad04450cfad8a83520577 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Tue, 18 Aug 2026 15:26:40 -0700 Subject: [PATCH] refactor(ui): move the cache settings and playground model selector off tremor (#37323) * 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. --- ui/litellm-dashboard/eslint-suppressions.json | 5 +--- .../cache_settings/index.integration.test.tsx | 13 +++++++++ .../_components/cache_settings/index.tsx | 2 +- .../_components/add_margin_form.test.tsx | 27 +++++++++++++++++++ .../_components/add_margin_form.tsx | 2 +- .../_components/add_provider_form.test.tsx | 9 +++++++ ...ost_tracking_settings.integration.test.tsx | 13 +++------ .../_components/cost_tracking_settings.tsx | 4 +-- .../components/ModelSelector.test.tsx | 14 +++++++++- .../compareUI/components/ModelSelector.tsx | 6 ++--- 10 files changed, 73 insertions(+), 22 deletions(-) diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 7dac21708e9..e467ddc5ec4 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -161,9 +161,6 @@ "local/filename-pascal-case": { "count": 1 }, - "no-restricted-imports": { - "count": 2 - }, "react-hooks/set-state-in-effect": { "count": 1 } @@ -959,7 +956,7 @@ }, "src/app/(dashboard)/playground/components/compareUI/components/ModelSelector.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 } }, "src/app/(dashboard)/playground/components/complianceUI/ComplianceUI.tsx": { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_settings/index.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_settings/index.integration.test.tsx index bcbc3acc7d8..8029dff0c9e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_settings/index.integration.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_settings/index.integration.test.tsx @@ -63,6 +63,19 @@ describe("CacheSettings advanced settings round-trip", () => { }); }); + it("reveals the advanced field sections only after the user expands them", async () => { + const user = userEvent.setup(); + renderSettings(); + await screen.findByText("Connection Settings"); + expect(screen.queryByText("SSL Settings")).not.toBeInTheDocument(); + + await user.click(screen.getByRole("button", { name: "Advanced Settings" })); + + expect(await screen.findByText("SSL Settings")).toBeInTheDocument(); + expect(screen.getByText("Cache Management")).toBeInTheDocument(); + expect(screen.getByText("GCP Authentication")).toBeInTheDocument(); + }); + it("sends the same payload whether or not the advanced section was expanded", async () => { const user = userEvent.setup(); renderSettings(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_settings/index.tsx b/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_settings/index.tsx index 76ec1b2c4cc..d26cc1b40f8 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_settings/index.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/caching/_components/cache_settings/index.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useCallback } from "react"; import { ChevronRight } from "lucide-react"; import { FormProvider, useForm } from "react-hook-form"; -import { Button } from "@tremor/react"; +import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { getCacheSettingsCall, testCacheConnectionCall, updateCacheSettingsCall } from "@/components/networking"; import { fetchAvailableModels, ModelGroup } from "@/components/llm_calls/fetch_models"; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/add_margin_form.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/add_margin_form.test.tsx index dc2e79d3199..69115346451 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/add_margin_form.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/add_margin_form.test.tsx @@ -95,6 +95,33 @@ describe("AddMarginForm", () => { expect(onAddProvider).toHaveBeenCalledTimes(1); }); + it("should report the edited percentage as the user types", async () => { + const onPercentageChange = vi.fn(); + const user = userEvent.setup(); + renderWithProviders( + , + ); + + await user.type(screen.getByPlaceholderText("10"), "0"); + expect(onPercentageChange).toHaveBeenCalledWith("10"); + }); + + it("should report the edited fixed amount as the user types", async () => { + const onFixedAmountChange = vi.fn(); + const user = userEvent.setup(); + renderWithProviders( + , + ); + + await user.type(screen.getByPlaceholderText("0.001"), "1"); + expect(onFixedAmountChange).toHaveBeenCalledWith("0.001"); + }); + it("should call onMarginTypeChange when the Fixed Amount radio is clicked", async () => { const onMarginTypeChange = vi.fn(); const user = userEvent.setup(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/add_margin_form.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/add_margin_form.tsx index 7fc1559d683..c307c176122 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/add_margin_form.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/add_margin_form.tsx @@ -172,7 +172,7 @@ const AddMarginForm: React.FC = ({
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/ModelSelector.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/ModelSelector.test.tsx index b6965da60fe..9c01521e77a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/ModelSelector.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/ModelSelector.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; import { ModelSelector } from "./ModelSelector"; @@ -28,6 +28,18 @@ describe("ModelSelector", () => { expect(screen.getByTitle("custom-model-123")).toHaveTextContent("custom-model-123"); }); + it("reports a custom model typed into the custom name field", async () => { + const user = userEvent.setup(); + const onChange = vi.fn(); + render(); + + await user.click(screen.getByRole("combobox")); + fireEvent.click(await screen.findByTitle("+ Add custom model")); + await user.type(await screen.findByPlaceholderText("Custom Model Name (Enter to add)"), "my-custom-model{Enter}"); + + expect(onChange).toHaveBeenCalledWith("my-custom-model"); + }); + it("disables the control when disabled is set", () => { const { rerender } = render(); expect(screen.getByRole("combobox")).toBeEnabled(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/ModelSelector.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/ModelSelector.tsx index ff7fe18ade2..c88560ab4f5 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/ModelSelector.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/ModelSelector.tsx @@ -1,6 +1,6 @@ import React, { useMemo, useState } from "react"; import { Select } from "antd"; -import { TextInput } from "@tremor/react"; +import { Input } from "@/components/ui/input"; interface ModelSelectorProps { value: string; onChange: (value: string) => void; @@ -68,11 +68,11 @@ export function ModelSelector({ value, onChange, models, loading, disabled }: Mo + Add custom model {isAddingCustom && ( - setCustomValue(e.target.value)} onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault();