diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.test.tsx index 6891082b47e..1f933d42e06 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.test.tsx @@ -113,12 +113,15 @@ const MOCK_AUTHORIZED = { }; const mockSetSelectedModelGroup = vi.fn(); +const mockSetSelectedModelAccessGroupFilter = vi.fn(); const mockSetSelectedModelId = vi.fn(); const mockSetSelectedTeamId = vi.fn(); const defaultProps = { selectedModelGroup: "all", setSelectedModelGroup: mockSetSelectedModelGroup, + selectedModelAccessGroupFilter: null, + setSelectedModelAccessGroupFilter: mockSetSelectedModelAccessGroupFilter, availableModelGroups: ["gpt-4", "gpt-3.5-turbo"], availableModelAccessGroups: ["sales-team"], setSelectedModelId: mockSetSelectedModelId, @@ -263,6 +266,35 @@ describe("AllModelsTab", () => { expect(within(table).queryByText("gpt-4")).not.toBeInTheDocument(); }); + it("filters the fetched page down to the selected access group", () => { + setModelsInfo( + [ + makeRow(), + { + ...makeRow({ model_info: { id: "model-2", access_groups: ["sales-team"] } }), + model_name: "claude-opus", + }, + ], + 2, + ); + render(); + + const table = screen.getByRole("table"); + expect(within(table).getByText("claude-opus")).toBeInTheDocument(); + expect(within(table).queryByText("gpt-4")).not.toBeInTheDocument(); + }); + + it("removing the access group filter chip clears it through the setter", async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByTestId("filter-chip-remove-model_info_access_groups")); + + await waitFor(() => { + expect(mockSetSelectedModelAccessGroupFilter).toHaveBeenCalledWith(null); + }); + }); + it("keeps an unknown model group (for example a stale deep link) visible as a removable filter chip", async () => { const user = userEvent.setup(); setModelsInfo([makeRow()], 1); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx index 1dc7736d5ac..b56e3fcb19d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx @@ -32,6 +32,8 @@ const DEFAULT_PAGINATION: PaginationState = { pageIndex: 0, pageSize: DEFAULT_PA interface AllModelsTabProps { selectedModelGroup: string | null; setSelectedModelGroup: (selectedModelGroup: string) => void; + selectedModelAccessGroupFilter: string | null; + setSelectedModelAccessGroupFilter: (selectedModelAccessGroupFilter: string | null) => void; availableModelGroups: string[]; availableModelAccessGroups: string[]; setSelectedModelId: (id: string) => void; @@ -41,6 +43,8 @@ interface AllModelsTabProps { const AllModelsTab = ({ selectedModelGroup, setSelectedModelGroup, + selectedModelAccessGroupFilter, + setSelectedModelAccessGroupFilter, availableModelGroups, availableModelAccessGroups, setSelectedModelId, @@ -55,7 +59,6 @@ const AllModelsTab = ({ const [debouncedSearch, setDebouncedSearch] = useState(""); const [modelViewMode, setModelViewMode] = useState("current_team"); const [selectedTeamValue, setSelectedTeamValue] = useState(PERSONAL_TEAM_VALUE); - const [selectedModelAccessGroupFilter, setSelectedModelAccessGroupFilter] = useState(null); const [pagination, setPagination] = useState(DEFAULT_PAGINATION); const [sorting, setSorting] = useState([]); const [isModelSettingsModalVisible, setIsModelSettingsModalVisible] = useState(false); 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 index 3b2c0a84ea2..454b2abfecb 100644 --- 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 @@ -7,6 +7,8 @@ import AllModelsPanel from "./AllModelsPanel"; interface CapturedTabProps { selectedModelGroup: string | null; setSelectedModelGroup: (modelGroup: string) => void; + selectedModelAccessGroupFilter: string | null; + setSelectedModelAccessGroupFilter: (accessGroup: string | null) => void; } const captured: { props: CapturedTabProps | null } = { props: null }; @@ -59,3 +61,47 @@ describe("AllModelsPanel ?model_group= filter", () => { spy.mockRestore(); }); }); + +describe("AllModelsPanel ?model_access_group= filter", () => { + beforeEach(() => { + captured.props = null; + window.history.pushState(null, "", "/models-and-endpoints/"); + }); + + it("seeds the access group filter from the URL", () => { + window.history.pushState(null, "", "/models-and-endpoints/?model_access_group=sales-team"); + render(); + expect(captured.props?.selectedModelAccessGroupFilter).toBe("sales-team"); + }); + + it("writes the selected access group to the URL via replaceState", () => { + render(); + const spy = vi.spyOn(window.history, "replaceState"); + act(() => captured.props?.setSelectedModelAccessGroupFilter("sales-team")); + expect(spy.mock.calls.at(-1)?.[2]).toContain("model_access_group=sales-team"); + spy.mockRestore(); + }); + + it("removes the param when the access group filter clears", () => { + window.history.pushState(null, "", "/models-and-endpoints/?model_access_group=sales-team"); + render(); + const spy = vi.spyOn(window.history, "replaceState"); + act(() => captured.props?.setSelectedModelAccessGroupFilter(null)); + expect(spy.mock.calls.at(-1)?.[2] as string).not.toContain("model_access_group"); + spy.mockRestore(); + }); + + it("the two filter params stay independent", () => { + window.history.pushState(null, "", "/models-and-endpoints/?model_group=gpt-4o&model_access_group=sales-team"); + render(); + expect(captured.props?.selectedModelGroup).toBe("gpt-4o"); + expect(captured.props?.selectedModelAccessGroupFilter).toBe("sales-team"); + + const spy = vi.spyOn(window.history, "replaceState"); + act(() => captured.props?.setSelectedModelAccessGroupFilter(null)); + const url = spy.mock.calls.at(-1)?.[2] as string; + expect(url).toContain("model_group=gpt-4o"); + expect(url).not.toContain("model_access_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 f6021e92ede..fd6a4607124 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 @@ -8,18 +8,27 @@ import { useModelDashboardData } from "@/app/(dashboard)/models-and-endpoints/us import { useModelDetailRouting } from "@/app/(dashboard)/models-and-endpoints/detailNavigation"; import { navigateWithParams } from "@/app/(dashboard)/navigateWithParams"; -export default function AllModelsPanel() { +const useUrlFilter = (paramName: string): [string | null, (value: string | null) => void] => { 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 value = searchParams?.get(paramName) ?? null; + const setValue = useCallback( + (nextValue: string | null) => { + navigateWithParams((params) => { + if (nextValue && nextValue !== ALL_MODEL_GROUPS_VALUE) { + params.set(paramName, nextValue); + } else { + params.delete(paramName); + } + }, "replace"); + }, + [paramName], + ); + return [value, setValue]; +}; + +export default function AllModelsPanel() { + const [selectedModelGroup, setSelectedModelGroup] = useUrlFilter("model_group"); + const [selectedModelAccessGroupFilter, setSelectedModelAccessGroupFilter] = useUrlFilter("model_access_group"); const { availableModelGroups, availableModelAccessGroups } = useModelDashboardData(); const { openModel, openTeam } = useModelDetailRouting(); @@ -27,6 +36,8 @@ export default function AllModelsPanel() {