mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
feat(ui): drive the model access group filter from ?model_access_group=
Keeps URL parity with the ?model_group= filter on the same page: the access group drawer filter now reads from and writes to the URL the same way, so both filters are shareable and clear together with their chips. The filter state moves from AllModelsTab local state up into AllModelsPanel where the URL wiring lives
This commit is contained in:
parent
90739e41ce
commit
0add3eda4b
4 changed files with 104 additions and 12 deletions
|
|
@ -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(<AllModelsTab {...defaultProps} selectedModelAccessGroupFilter="sales-team" />);
|
||||
|
||||
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(<AllModelsTab {...defaultProps} selectedModelAccessGroupFilter="sales-team" />);
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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<string>("");
|
||||
const [modelViewMode, setModelViewMode] = useState<ModelViewMode>("current_team");
|
||||
const [selectedTeamValue, setSelectedTeamValue] = useState<string>(PERSONAL_TEAM_VALUE);
|
||||
const [selectedModelAccessGroupFilter, setSelectedModelAccessGroupFilter] = useState<string | null>(null);
|
||||
const [pagination, setPagination] = useState<PaginationState>(DEFAULT_PAGINATION);
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [isModelSettingsModalVisible, setIsModelSettingsModalVisible] = useState(false);
|
||||
|
|
|
|||
|
|
@ -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(<AllModelsPanel />);
|
||||
expect(captured.props?.selectedModelAccessGroupFilter).toBe("sales-team");
|
||||
});
|
||||
|
||||
it("writes the selected access group to the URL via replaceState", () => {
|
||||
render(<AllModelsPanel />);
|
||||
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(<AllModelsPanel />);
|
||||
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(<AllModelsPanel />);
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<AllModelsTab
|
||||
selectedModelGroup={selectedModelGroup}
|
||||
setSelectedModelGroup={setSelectedModelGroup}
|
||||
selectedModelAccessGroupFilter={selectedModelAccessGroupFilter}
|
||||
setSelectedModelAccessGroupFilter={setSelectedModelAccessGroupFilter}
|
||||
availableModelGroups={availableModelGroups}
|
||||
availableModelAccessGroups={availableModelAccessGroups}
|
||||
setSelectedModelId={openModel}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue