diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AllModelsPanel.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AllModelsPanel.test.tsx new file mode 100644 index 00000000000..3b2c0a84ea2 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AllModelsPanel.test.tsx @@ -0,0 +1,61 @@ +/* @vitest-environment jsdom */ +import { act, render } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import AllModelsPanel from "./AllModelsPanel"; + +interface CapturedTabProps { + selectedModelGroup: string | null; + setSelectedModelGroup: (modelGroup: string) => void; +} + +const captured: { props: CapturedTabProps | null } = { props: null }; + +vi.mock("@/app/(dashboard)/models-and-endpoints/components/AllModelsTab", () => ({ + default: (props: CapturedTabProps) => { + captured.props = props; + return
; + }, +})); +vi.mock("@/app/(dashboard)/models-and-endpoints/components/AllModelsTable", () => ({ + ALL_MODEL_GROUPS_VALUE: "all", +})); +vi.mock("@/app/(dashboard)/models-and-endpoints/useModelDashboardData", () => ({ + useModelDashboardData: () => ({ availableModelGroups: [], availableModelAccessGroups: [] }), +})); +vi.mock("next/navigation", () => ({ useSearchParams: () => new URLSearchParams(window.location.search) })); + +describe("AllModelsPanel ?model_group= filter", () => { + beforeEach(() => { + captured.props = null; + window.history.pushState(null, "", "/models-and-endpoints/"); + }); + + it("seeds the model group filter from the URL", () => { + window.history.pushState(null, "", "/models-and-endpoints/?model_group=gpt-4o"); + render(); + expect(captured.props?.selectedModelGroup).toBe("gpt-4o"); + }); + + it("defaults to no filter when the param is absent", () => { + render(); + expect(captured.props?.selectedModelGroup).toBeNull(); + }); + + it("writes the selected group to the URL via replaceState", () => { + render(); + const spy = vi.spyOn(window.history, "replaceState"); + act(() => captured.props?.setSelectedModelGroup("claude-opus-5")); + expect(spy.mock.calls.at(-1)?.[2]).toContain("model_group=claude-opus-5"); + spy.mockRestore(); + }); + + it("removes the param when the filter resets to all groups", () => { + window.history.pushState(null, "", "/models-and-endpoints/?model_group=gpt-4o"); + render(); + const spy = vi.spyOn(window.history, "replaceState"); + act(() => captured.props?.setSelectedModelGroup("all")); + expect(spy.mock.calls.at(-1)?.[2] as string).not.toContain("model_group"); + spy.mockRestore(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AllModelsPanel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AllModelsPanel.tsx index 9d40ea32185..f6021e92ede 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AllModelsPanel.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AllModelsPanel.tsx @@ -1,12 +1,25 @@ "use client"; -import { useState } from "react"; +import { useCallback } from "react"; +import { useSearchParams } from "next/navigation"; import AllModelsTab from "@/app/(dashboard)/models-and-endpoints/components/AllModelsTab"; +import { ALL_MODEL_GROUPS_VALUE } from "@/app/(dashboard)/models-and-endpoints/components/AllModelsTable"; import { useModelDashboardData } from "@/app/(dashboard)/models-and-endpoints/useModelDashboardData"; import { useModelDetailRouting } from "@/app/(dashboard)/models-and-endpoints/detailNavigation"; +import { navigateWithParams } from "@/app/(dashboard)/navigateWithParams"; export default function AllModelsPanel() { - const [selectedModelGroup, setSelectedModelGroup] = useState(null); + const searchParams = useSearchParams(); + const selectedModelGroup = searchParams?.get("model_group") ?? null; + const setSelectedModelGroup = useCallback((modelGroup: string) => { + navigateWithParams((params) => { + if (modelGroup && modelGroup !== ALL_MODEL_GROUPS_VALUE) { + params.set("model_group", modelGroup); + } else { + params.delete("model_group"); + } + }, "replace"); + }, []); const { availableModelGroups, availableModelAccessGroups } = useModelDashboardData(); const { openModel, openTeam } = useModelDetailRouting();