fix(ui): render caller-supplied filter options in caller order (LIT-3151) (#29462)
FilterComponent iterated a hardcoded orderedFilters whitelist instead of the options prop, so any consumer whose filter names were not on that list rendered nothing. The Tool Policies page passes "Input Policy", "Output Policy", "Team Name" and "Key Name", none of which were whitelisted, so its Filters panel opened to an empty area. Drop the whitelist and render the options the caller passes, in the order they pass them, so each page owns its own filter set and ordering. The Logs page array is reordered to match its prior on-screen order; VirtualKeys and TeamVirtualKeys already matched the old whitelist order and are unaffected.
This commit is contained in:
parent
1cce49b9d0
commit
609e1e9763
@ -103,7 +103,7 @@ describe("FilterComponent", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should render filters in correct order", async () => {
|
||||
it("renders filters in the caller-supplied order", async () => {
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const options: FilterOption[] = [
|
||||
{ name: "model", label: "Model" },
|
||||
@ -113,11 +113,7 @@ describe("FilterComponent", () => {
|
||||
];
|
||||
|
||||
renderWithProviders(
|
||||
<FilterComponent
|
||||
options={options}
|
||||
onApplyFilters={mockOnApplyFilters}
|
||||
onResetFilters={mockOnResetFilters}
|
||||
/>,
|
||||
<FilterComponent options={options} onApplyFilters={mockOnApplyFilters} onResetFilters={mockOnResetFilters} />,
|
||||
);
|
||||
|
||||
const filterButton = screen.getByRole("button", { name: "Filters" });
|
||||
@ -125,8 +121,7 @@ describe("FilterComponent", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
const labels = screen.getAllByText(/^(Team ID|Status|User ID|Model)$/);
|
||||
expect(labels[0]).toHaveTextContent("Team ID");
|
||||
expect(labels[1]).toHaveTextContent("Status");
|
||||
expect(labels.map((l) => l.textContent)).toEqual(["Model", "Team ID", "Status", "User ID"]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -218,11 +213,7 @@ describe("FilterComponent", () => {
|
||||
];
|
||||
|
||||
renderWithProviders(
|
||||
<FilterComponent
|
||||
options={options}
|
||||
onApplyFilters={mockOnApplyFilters}
|
||||
onResetFilters={mockOnResetFilters}
|
||||
/>,
|
||||
<FilterComponent options={options} onApplyFilters={mockOnApplyFilters} onResetFilters={mockOnResetFilters} />,
|
||||
);
|
||||
|
||||
const filterButton = screen.getByRole("button", { name: "Filters" });
|
||||
@ -245,9 +236,7 @@ describe("FilterComponent", () => {
|
||||
|
||||
it("should debounce search input for searchable filters", async () => {
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const mockSearchFn = vi.fn().mockResolvedValue([
|
||||
{ label: "Result", value: "result" },
|
||||
]);
|
||||
const mockSearchFn = vi.fn().mockResolvedValue([{ label: "Result", value: "result" }]);
|
||||
|
||||
const options: FilterOption[] = [
|
||||
{
|
||||
@ -259,11 +248,7 @@ describe("FilterComponent", () => {
|
||||
];
|
||||
|
||||
renderWithProviders(
|
||||
<FilterComponent
|
||||
options={options}
|
||||
onApplyFilters={mockOnApplyFilters}
|
||||
onResetFilters={mockOnResetFilters}
|
||||
/>,
|
||||
<FilterComponent options={options} onApplyFilters={mockOnApplyFilters} onResetFilters={mockOnResetFilters} />,
|
||||
);
|
||||
|
||||
const filterButton = screen.getByRole("button", { name: "Filters" });
|
||||
@ -311,11 +296,7 @@ describe("FilterComponent", () => {
|
||||
];
|
||||
|
||||
renderWithProviders(
|
||||
<FilterComponent
|
||||
options={options}
|
||||
onApplyFilters={mockOnApplyFilters}
|
||||
onResetFilters={mockOnResetFilters}
|
||||
/>,
|
||||
<FilterComponent options={options} onApplyFilters={mockOnApplyFilters} onResetFilters={mockOnResetFilters} />,
|
||||
);
|
||||
|
||||
const filterButton = screen.getByRole("button", { name: "Filters" });
|
||||
@ -360,11 +341,7 @@ describe("FilterComponent", () => {
|
||||
];
|
||||
|
||||
renderWithProviders(
|
||||
<FilterComponent
|
||||
options={options}
|
||||
onApplyFilters={mockOnApplyFilters}
|
||||
onResetFilters={mockOnResetFilters}
|
||||
/>,
|
||||
<FilterComponent options={options} onApplyFilters={mockOnApplyFilters} onResetFilters={mockOnResetFilters} />,
|
||||
);
|
||||
|
||||
const filterButton = screen.getByRole("button", { name: "Filters" });
|
||||
@ -393,9 +370,7 @@ describe("FilterComponent", () => {
|
||||
|
||||
it("should load initial options when dropdown opens for searchable filter", async () => {
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const mockSearchFn = vi.fn().mockResolvedValue([
|
||||
{ label: "Initial Result", value: "initial" },
|
||||
]);
|
||||
const mockSearchFn = vi.fn().mockResolvedValue([{ label: "Initial Result", value: "initial" }]);
|
||||
|
||||
const options: FilterOption[] = [
|
||||
{
|
||||
@ -407,11 +382,7 @@ describe("FilterComponent", () => {
|
||||
];
|
||||
|
||||
renderWithProviders(
|
||||
<FilterComponent
|
||||
options={options}
|
||||
onApplyFilters={mockOnApplyFilters}
|
||||
onResetFilters={mockOnResetFilters}
|
||||
/>,
|
||||
<FilterComponent options={options} onApplyFilters={mockOnApplyFilters} onResetFilters={mockOnResetFilters} />,
|
||||
);
|
||||
|
||||
const filterButton = screen.getByRole("button", { name: "Filters" });
|
||||
@ -433,7 +404,7 @@ describe("FilterComponent", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should not render filters that are not in orderedFilters list", async () => {
|
||||
it("renders caller-supplied options that match no predefined filter name (LIT-3151)", async () => {
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const options: FilterOption[] = [
|
||||
{
|
||||
@ -443,18 +414,36 @@ describe("FilterComponent", () => {
|
||||
];
|
||||
|
||||
renderWithProviders(
|
||||
<FilterComponent
|
||||
options={options}
|
||||
onApplyFilters={mockOnApplyFilters}
|
||||
onResetFilters={mockOnResetFilters}
|
||||
/>,
|
||||
<FilterComponent options={options} onApplyFilters={mockOnApplyFilters} onResetFilters={mockOnResetFilters} />,
|
||||
);
|
||||
|
||||
const filterButton = screen.getByRole("button", { name: "Filters" });
|
||||
await user.click(filterButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("Unknown Filter")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Unknown Filter")).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText("Enter Unknown Filter...")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders every Tool Policies filter when none match a predefined name (LIT-3151)", async () => {
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const options: FilterOption[] = [
|
||||
{ name: "Input Policy", label: "Input Policy", options: [{ label: "Trusted", value: "trusted" }] },
|
||||
{ name: "Output Policy", label: "Output Policy", options: [{ label: "Blocked", value: "blocked" }] },
|
||||
{ name: "Team Name", label: "Team Name", options: [] },
|
||||
{ name: "Key Name", label: "Key Name", options: [] },
|
||||
];
|
||||
|
||||
renderWithProviders(
|
||||
<FilterComponent options={options} onApplyFilters={mockOnApplyFilters} onResetFilters={mockOnResetFilters} />,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Filters" }));
|
||||
|
||||
await waitFor(() => {
|
||||
const labels = screen.getAllByText(/^(Input Policy|Output Policy|Team Name|Key Name)$/);
|
||||
expect(labels.map((l) => l.textContent)).toEqual(["Input Policy", "Output Policy", "Team Name", "Key Name"]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -129,21 +129,6 @@ const FilterComponent: React.FC<FilterComponentProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Define the order of filters
|
||||
const orderedFilters = [
|
||||
"Team ID",
|
||||
"Status",
|
||||
"Organization ID",
|
||||
"Key Alias",
|
||||
"User ID",
|
||||
"End User",
|
||||
"Error Code",
|
||||
"Error Message",
|
||||
"Key Hash",
|
||||
"Model",
|
||||
"Public model / search tool",
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
@ -159,10 +144,7 @@ const FilterComponent: React.FC<FilterComponentProps> = ({
|
||||
|
||||
{showFilters && (
|
||||
<div className="grid grid-cols-3 gap-x-6 gap-y-4 mb-6">
|
||||
{orderedFilters.map((filterName) => {
|
||||
const option = options.find((opt) => opt.label === filterName || opt.name === filterName);
|
||||
if (!option) return null;
|
||||
|
||||
{options.map((option) => {
|
||||
return (
|
||||
<div key={option.name} className="flex flex-col gap-2">
|
||||
<label className="text-sm text-gray-600">{option.label || option.name}</label>
|
||||
|
||||
@ -22,16 +22,6 @@ export function getLogFilterOptions(accessToken: string): FilterOption[] {
|
||||
{ label: "Failure", value: "failure" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Model",
|
||||
label: "Model",
|
||||
customComponent: PaginatedModelSelect,
|
||||
},
|
||||
{
|
||||
name: FILTER_KEYS.PUBLIC_MODEL_OR_SEARCH_TOOL,
|
||||
label: "Public model / search tool",
|
||||
isSearchable: false,
|
||||
},
|
||||
{
|
||||
name: "Key Alias",
|
||||
label: "Key Alias",
|
||||
@ -63,14 +53,24 @@ export function getLogFilterOptions(accessToken: string): FilterOption[] {
|
||||
return filtered;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Error Message",
|
||||
label: "Error Message",
|
||||
isSearchable: false,
|
||||
},
|
||||
{
|
||||
name: "Key Hash",
|
||||
label: "Key Hash",
|
||||
isSearchable: false,
|
||||
},
|
||||
{
|
||||
name: "Error Message",
|
||||
label: "Error Message",
|
||||
name: "Model",
|
||||
label: "Model",
|
||||
customComponent: PaginatedModelSelect,
|
||||
},
|
||||
{
|
||||
name: FILTER_KEYS.PUBLIC_MODEL_OR_SEARCH_TOOL,
|
||||
label: "Public model / search tool",
|
||||
isSearchable: false,
|
||||
},
|
||||
];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user