fix(logs): preserve exact alias exclusions when editing filters

This commit is contained in:
anastasio 2026-10-13 13:48:07 +08:00
parent c875836ed5
commit 333fd5b773
6 changed files with 48 additions and 9 deletions

View file

@ -47,8 +47,8 @@ function renderFilters(filters: Record<string, string> = {}) {
return { set };
}
function StatefulFilters() {
const [filters, setFilters] = useState<Record<string, string | undefined>>({});
function StatefulFilters({ initialFilters = {} }: { initialFilters?: Record<string, string | undefined> }) {
const [filters, setFilters] = useState<Record<string, string | undefined>>(initialFilters);
return (
<RequestLogsFilters
get={(id: string) => filters[id]}
@ -213,6 +213,22 @@ describe("RequestLogsFilters", () => {
expect(set).toHaveBeenCalledWith(LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS, "noisy");
});
it("keeps a restored is not operator when clearing the alias to choose another", async () => {
vi.mocked(useInfiniteKeyAliases).mockReturnValue({
...emptyInfiniteQuery,
data: { pages: [{ aliases: ["noisy", "quiet"] }], pageParams: [1] },
} as unknown as ReturnType<typeof useInfiniteKeyAliases>);
const user = userEvent.setup();
renderWithProviders(<StatefulFilters initialFilters={{ [LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS]: "noisy" }} />);
await user.click(screen.getByRole("button", { name: "Clear", hidden: true }));
expect(screen.getByRole("combobox", { name: "Key Alias operator" })).toHaveTextContent("is not");
await chooseSelectOption(user, screen.getByPlaceholderText("Search a key alias"), "quiet");
expect(screen.getByRole("combobox", { name: "Key Alias operator" })).toHaveTextContent("is not");
expect(screen.getByPlaceholderText("Search a key alias")).toHaveValue("quiet");
});
it("does not leak the team scope into the Model lookup", async () => {
renderFilters({ [LOG_FILTER_IDS.TEAM_ID]: "team-42" });

View file

@ -108,7 +108,7 @@ function KeyAliasFilterField({
teamId: string;
}) {
const [search, setSearch] = useState("");
const [operator, setOperator] = useState<KeyAliasOperator>("is");
const [operator, setOperator] = useState<KeyAliasOperator>(excludeValue !== "" ? "is not" : "is");
let activeOperator: KeyAliasOperator = operator;
if (excludeValue !== "") {
activeOperator = "is not";

View file

@ -566,6 +566,21 @@ describe("RequestLogsPanel", () => {
await waitFor(() => expect(lastCall()?.params?.exclude_key_alias).toBeUndefined());
});
it.each([" noisy ", " "])(
"preserves the exact excluded alias %j when reapplying bookmarked filters",
async (alias) => {
const user = userEvent.setup();
renderPanel(new URLSearchParams({ exclude_key_alias: alias }).toString());
await waitFor(() => expect(lastCall()?.params?.exclude_key_alias).toBe(alias));
await user.click(screen.getByTestId("datatable-filters-trigger"));
await user.click(screen.getByTestId("filter-drawer-apply"));
await waitFor(() => expect(urlParams().get("exclude_key_alias")).toBe(alias));
expect(lastCall()?.params?.exclude_key_alias).toBe(alias);
},
);
it("treats conflicting Key Alias URL parameters as one mutually exclusive filter", async () => {
renderPanel("key_alias=quiet&exclude_key_alias=noisy");

View file

@ -18,6 +18,7 @@ import type { LogEntry } from "../types";
import {
DEFAULT_LOGS_SORTING,
formatLogsWindow,
getExactFilterValue,
getFilterValue,
getLogsWindowEndBound,
LOG_FILTER_IDS,
@ -224,7 +225,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
(updaterOrValue) => {
const next = typeof updaterOrValue === "function" ? updaterOrValue(columnFiltersRef.current) : updaterOrValue;
const nextKeyAlias = getFilterValue(next, LOG_FILTER_IDS.KEY_ALIAS);
const nextExcludeKeyAlias = getFilterValue(next, LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS);
const nextExcludeKeyAlias = getExactFilterValue(next, LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS);
const normalizedNext = next.filter(
(filter) => filter.id !== LOG_FILTER_IDS.KEY_ALIAS && filter.id !== LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS,
);

View file

@ -92,7 +92,9 @@ describe("useLogFilterLogic", () => {
{ id: LOG_FILTER_IDS.MODEL_ID, value: "model-uuid-1", param: "model_id" },
{ id: LOG_FILTER_IDS.PUBLIC_MODEL_OR_SEARCH_TOOL, value: "gpt-4o", param: "model" },
{ id: LOG_FILTER_IDS.KEY_ALIAS, value: "alias-1", param: "key_alias" },
{ id: "exclude_key_alias", value: "alias-1", param: "exclude_key_alias" },
{ id: LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS, value: "alias-1", param: "exclude_key_alias" },
{ id: LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS, value: " alias-1 ", param: "exclude_key_alias" },
{ id: LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS, value: " ", param: "exclude_key_alias" },
{ id: LOG_FILTER_IDS.ERROR_CODE, value: "429", param: "error_code" },
{ id: LOG_FILTER_IDS.ERROR_MESSAGE, value: "rate limited", param: "error_message" },
{ id: LOG_FILTER_IDS.USER_ID, value: "user-9", param: "user_id" },
@ -111,6 +113,7 @@ describe("useLogFilterLogic", () => {
columnFilters: [
{ id: LOG_FILTER_IDS.TEAM_ID, value: " " },
{ id: LOG_FILTER_IDS.KEY_HASH, value: "" },
{ id: LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS, value: "" },
],
});
@ -118,6 +121,7 @@ describe("useLogFilterLogic", () => {
const params = lastCallParams()?.params;
expect(params?.team_id).toBeUndefined();
expect(params?.api_key).toBeUndefined();
expect(params?.exclude_key_alias).toBeUndefined();
expect(params?.error_code).toBeUndefined();
});
});

View file

@ -98,10 +98,13 @@ export const DEFAULT_LOGS_SORTING: SortingState = [{ id: "startTime", desc: true
const isSortField = (id: string): id is LogsSortField => Object.hasOwn(LOGS_SORT_FIELD_MAP, id);
export const getFilterValue = (columnFilters: ColumnFiltersState, columnId: string): string | undefined => {
export const getExactFilterValue = (columnFilters: ColumnFiltersState, columnId: string): string | undefined => {
const entry = columnFilters.find((filter) => filter.id === columnId);
if (typeof entry?.value !== "string") return undefined;
const trimmed = entry.value.trim();
return typeof entry?.value === "string" && entry.value !== "" ? entry.value : undefined;
};
export const getFilterValue = (columnFilters: ColumnFiltersState, columnId: string): string | undefined => {
const trimmed = getExactFilterValue(columnFilters, columnId)?.trim();
return trimmed === "" ? undefined : trimmed;
};
@ -194,7 +197,7 @@ export function useLogFilterLogic({
model_id: getFilterValue(columnFilters, LOG_FILTER_IDS.MODEL_ID),
model: getFilterValue(columnFilters, LOG_FILTER_IDS.PUBLIC_MODEL_OR_SEARCH_TOOL),
key_alias: getFilterValue(columnFilters, LOG_FILTER_IDS.KEY_ALIAS),
exclude_key_alias: getFilterValue(columnFilters, LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS),
exclude_key_alias: getExactFilterValue(columnFilters, LOG_FILTER_IDS.EXCLUDE_KEY_ALIAS),
error_code: getFilterValue(columnFilters, LOG_FILTER_IDS.ERROR_CODE),
error_message: getFilterValue(columnFilters, LOG_FILTER_IDS.ERROR_MESSAGE),
sort_by: sortBy,