feat(site): add shared DateTimeRangeFilter component - #28255
Merged
Conversation
stringifyFilter only quoted values containing spaces, so filter values like RFC 3339 timestamps (2026-08-16T20:42:00Z) were emitted unquoted and the backend query parser rejected their colons. Quote values containing colons too; they were never valid unquoted. Extract parseFilterQuery/stringifyFilter into filterQuery.ts with unit tests.
A text-expression datetime range picker with From/To inputs accepting now, clock times, dates, or date and time pairs, with inline errors, blur normalization, and clamping. Moved to site/src/components so other pages can reuse it.
This was referenced Aug 18, 2026
johnstcn
commented
Aug 18, 2026
Comment on lines
+72
to
+87
| export const formatTriggerLabel = (range: TimeRange, now: Date): string => { | ||
| const from = dayjs(range.startedAfter); | ||
| if (sameDay(range.startedAfter, range.startedBefore)) { | ||
| return from.format(MONTH_DAY); | ||
| } | ||
| if (sameDay(range.startedBefore, now)) { | ||
| return `${from.format(MONTH_DAY)} - Today`; | ||
| } | ||
| if ( | ||
| range.startedAfter.getFullYear() === range.startedBefore.getFullYear() && | ||
| range.startedAfter.getMonth() === range.startedBefore.getMonth() | ||
| ) { | ||
| return `${from.format(MONTH_DAY)} - ${range.startedBefore.getDate()}`; | ||
| } | ||
| return `${from.format(MONTH_DAY)} - ${dayjs(range.startedBefore).format(MONTH_DAY)}`; | ||
| }; |
Member
Author
There was a problem hiding this comment.
I'm open to suggestions on the actual copy here.
The shared component's parser and trigger-label logic lived in timeRange.ts with no unit tests; coverage was only via story play functions. Add vitest coverage for the expression grammar and the label derivation cases.
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a reusable, text-expression based datetime range filter UI to the frontend so multiple pages (notably the AI Gateway sessions page in the PR stack) can apply consistent started-after / started-before filtering with concise trigger labels.
Changes:
- Introduces
DateTimeRangeFilterpopover component with From/To expression inputs, inline validation, and an Apply action. - Adds parsing/labeling utilities (
parseTimeExpression,formatTriggerLabel) plus unit tests for these pure functions. - Adds Storybook stories with interaction coverage for the component’s key behaviors (prefill, blur normalization, clamping, validation, apply/cancel).
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| site/src/components/DateTimeRangeFilter/timeRange.ts | Implements strict parsing for supported time expressions and derives trigger labels from resolved ranges. |
| site/src/components/DateTimeRangeFilter/timeRange.test.ts | Unit tests for expression parsing and trigger label formatting. |
| site/src/components/DateTimeRangeFilter/DateTimeRangeFilter.tsx | New shared UI component: popover trigger, inputs, validation, normalization/clamping, and apply behavior. |
| site/src/components/DateTimeRangeFilter/DateTimeRangeFilter.stories.tsx | Storybook stories and interaction tests validating the UX and state transitions. |
Suppressed comments (2)
site/src/components/DateTimeRangeFilter/DateTimeRangeFilter.tsx:125
- Empty input is treated as non-error (fromError/toError stay null), so the Apply button can become enabled even though parsedFrom/parsedTo are null. Clicking Apply then closes the popover without applying anything. Also, Apply is enabled based only on "touched" (not actual value changes), which contradicts the comment and allows no-op applies after reverting edits.
const fromError =
fromField.text !== "" && parsedFrom === null ? INVALID_TIME_MESSAGE : null;
const toError =
toField.text !== "" && parsedTo === null ? INVALID_TIME_MESSAGE : null;
site/src/components/DateTimeRangeFilter/DateTimeRangeFilter.tsx:109
- normalize() currently skips clamping/normalizing whenever the current text is "now", so an out-of-order "now" boundary (for example, To="now" when From is in the future, or From="now" when To is in the past) will not clamp against its sibling as described. This leaves the user stuck with a range error until they manually edit the field away from "now".
setField((current) => {
if (isNowExpression(current.text) || parsed === null) {
return current;
}
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
johnstcn
added a commit
that referenced
this pull request
Aug 18, 2026
) `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)
Split the shared blur normalizer into normalizeFrom and normalizeTo, each reading its own parsed boundary from render scope. Source the parse formats from the shared DATE_FORMAT constants and drop the single-digit-hour variants so input is padded-only. Add a comment pointing future maintainers at formik and yup if the control grows.
…lter The component's isNowExpression duplicated the /^now$/i regex in parseTimeExpression. Export one predicate from timeRange.ts and use it in both the parser and the blur normalizers.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A text-expression datetime range picker with From/To inputs accepting
now, a clock time (current day), a date (midnight), or a date with a clock time. Invalid text gets inline errors, underspecified expressions resolve on blur, and out-of-order boundaries clamp against the other one. The trigger derives a concise label ("Last 24 hours", "Apr 10", "Aug 11 - Today", "Apr 17 - 19").Placed in
site/src/components/DateTimeRangeFilter/so other pages can reuse it. Expression parsing and trigger-label derivation live intimeRange.tsnext to the component; both are generic overDatepairs. Depends on #28254 for the sharedfilterQueryserialization helpers.Part of AIGOV-580
Generated by Coder Agents on behalf of @johnstcn.$
Stack: #28254 (filterQuery fix) \u2192 #28255 (component) \u2192 #28256 (sessions page)