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:
ryan-crabbe-berri 2026-07-29 17:14:09 -07:00
parent 90739e41ce
commit 0add3eda4b
4 changed files with 104 additions and 12 deletions

View file

@ -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);

View file

@ -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);

View file

@ -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();
});
});

View file

@ -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}