fix(site): quote colon-containing values in filter serialization (#28254)

`stringifyFilter` in the shared `Filter` component only quoted values
containing spaces. Filter values like RFC 3339 timestamps
(`2026-08-16T20:42:00Z`) contain colons but no spaces, so when any
filter was edited and the whole query re-serialized, the timestamp went
out unquoted and the backend `searchTerms` parser rejected it (`Query
element ... can only contain 1 ':'`).

Quote values containing colons as well; they were never valid unquoted
because the backend parser already rejects them. Extract
`parseFilterQuery`/`stringifyFilter` into `filterQuery.ts` with unit
tests, including a round-trip of quoted timestamps.

Part of
[AIGOV-580](https://linear.app/codercom/issue/AIGOV-580/ai-gateway-sessions-page-takes-5-10-seconds-to-load)

---
_Generated by Coder Agents on behalf of @johnstcn._$

---

**Stack:** #28254 (filterQuery fix) \u2192 #28255 (component) \u2192
#28256 (sessions page)
This commit is contained in:
Cian Johnston
2026-08-18 17:01:42 +01:00
committed by GitHub
parent 0db25caad6
commit 962366ffc6
3 changed files with 91 additions and 35 deletions
+5 -35
View File
@@ -26,14 +26,17 @@ import { SearchField } from "#/components/SearchField/SearchField";
import { Skeleton, type SkeletonProps } from "#/components/Skeleton/Skeleton";
import { useDebouncedFunction } from "#/hooks/debounce";
import { cn } from "#/utils/cn";
import {
type FilterValues,
parseFilterQuery,
stringifyFilter,
} from "./filterQuery";
type PresetFilter = {
name: string;
query: string;
};
type FilterValues = Record<string, string | undefined>;
type UseFilterConfig = {
/**
* The fallback value to use in the event that no filter params can be
@@ -102,39 +105,6 @@ export const useFilter = ({
};
};
const parseFilterQuery = (filterQuery: string): FilterValues => {
if (filterQuery === "") {
return {};
}
const result: FilterValues = {};
const keyValuePair = /(\w+):"([^"]+)"|(\w+):(\S+)/g;
for (const match of filterQuery.matchAll(keyValuePair)) {
const key = match[1] ?? match[3];
const value = match[2] ?? match[4];
if (key && value) {
result[key] = value;
}
}
return result;
};
const stringifyFilter = (filterValue: FilterValues): string => {
let result = "";
for (const key in filterValue) {
const value = filterValue[key];
if (value) {
const needsQuotes = value.includes(" ");
result += needsQuotes ? `${key}:"${value}" ` : `${key}:${value} `;
}
}
return result.trim();
};
const BaseSkeleton: FC<SkeletonProps> = ({ children, ...skeletonProps }) => {
return (
<Skeleton
@@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import { parseFilterQuery, stringifyFilter } from "./filterQuery";
describe("stringifyFilter", () => {
it("leaves simple values unquoted", () => {
expect(stringifyFilter({ initiator: "me" })).toBe("initiator:me");
});
it("quotes values containing spaces", () => {
expect(stringifyFilter({ session_id: "abc def" })).toBe(
'session_id:"abc def"',
);
});
it("quotes values containing colons so the backend parser accepts them", () => {
expect(stringifyFilter({ started_after: "2026-08-16T20:42:00Z" })).toBe(
'started_after:"2026-08-16T20:42:00Z"',
);
});
it("drops empty values", () => {
expect(stringifyFilter({ initiator: undefined, model: "gpt-4" })).toBe(
"model:gpt-4",
);
});
});
describe("parseFilterQuery", () => {
it("round-trips quoted timestamp values", () => {
const values = {
initiator: "me",
started_after: "2026-08-16T20:42:00Z",
started_before: "2026-08-17T20:42:00Z",
};
expect(parseFilterQuery(stringifyFilter(values))).toEqual(values);
});
it("parses unquoted and quoted pairs", () => {
expect(parseFilterQuery('initiator:me session_id:"abc def"')).toEqual({
initiator: "me",
session_id: "abc def",
});
});
it("returns an empty object for an empty query", () => {
expect(parseFilterQuery("")).toEqual({});
});
});
+38
View File
@@ -0,0 +1,38 @@
export type FilterValues = Record<string, string | undefined>;
export const parseFilterQuery = (filterQuery: string): FilterValues => {
if (filterQuery === "") {
return {};
}
const result: FilterValues = {};
const keyValuePair = /(\w+):"([^"]+)"|(\w+):(\S+)/g;
for (const match of filterQuery.matchAll(keyValuePair)) {
const key = match[1] ?? match[3];
const value = match[2] ?? match[4];
if (key && value) {
result[key] = value;
}
}
return result;
};
// Values containing spaces or colons must be quoted: the backend query
// parser splits unquoted elements on ':' and rejects more than one colon.
const needsQuotes = (value: string): boolean =>
value.includes(" ") || value.includes(":");
export const stringifyFilter = (filterValue: FilterValues): string => {
let result = "";
for (const key in filterValue) {
const value = filterValue[key];
if (value) {
result += needsQuotes(value) ? `${key}:"${value}" ` : `${key}:${value} `;
}
}
return result.trim();
};