From f3302883f5c351706d06d5a4092d130dd01b4e53 Mon Sep 17 00:00:00 2001 From: Tracy Johnson Date: Thu, 20 Aug 2026 22:41:37 +0000 Subject: [PATCH 1/4] feat(site/src/pages/AIBridgePage): use DateTimeRangePicker on the sessions page Replace the text-expression DateTimeRangeFilter with the new DateTimeRangePicker on the AI Gateway sessions page and drop the redundant Filters preset button. The preset queries (All sessions, My sessions) are already covered by the search field and the user dropdown. The picker preset id lives in component state only; the filter query keeps storing resolved started_after/started_before timestamps, so shared links and the API contract are unchanged. --- .../ListSessionsFilter.stories.tsx | 41 +++++++++++++------ .../ListSessionsPage/ListSessionsFilter.tsx | 28 ++++--------- .../ListSessionsPage/ListSessionsPage.tsx | 41 +++++++++++++++++-- .../ListSessionsPageView.stories.tsx | 10 ++--- 4 files changed, 80 insertions(+), 40 deletions(-) diff --git a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsFilter.stories.tsx b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsFilter.stories.tsx index 2a8f2aa4030..07249dc1a3f 100644 --- a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsFilter.stories.tsx +++ b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsFilter.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import type { ComponentProps } from "react"; -import { fn } from "storybook/test"; -import type { TimeRange } from "#/components/DateTimeRangeFilter/timeRange"; +import { expect, fn, screen, userEvent, waitFor, within } from "storybook/test"; +import type { DateTimeRangeValue } from "#/components/DateTimeRangePicker/dateTimeRange"; import { getDefaultFilterProps, MockMenu, @@ -12,17 +12,14 @@ type FilterProps = ComponentProps; type FilterAndMenus = Pick; -const timeRange: TimeRange = { - startedAfter: new Date("2026-08-12T15:00:00Z"), - startedBefore: new Date("2026-08-13T15:00:00Z"), +const timeRange: DateTimeRangeValue = { + start: new Date("2026-08-12T15:00:00Z"), + end: new Date("2026-08-13T15:00:00Z"), + preset: "last_24h", }; -const timeRangeProps: Pick< - FilterProps, - "timeRange" | "defaultTimeRange" | "onTimeRangeChange" -> = { +const timeRangeProps: Pick = { timeRange, - defaultTimeRange: timeRange, onTimeRangeChange: fn(), }; @@ -55,6 +52,26 @@ export const Default: Story = { args: { ...defaultFilterProps, }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // The preset queries were removed, so the Filters button is gone + // while the search field and dropdown menus remain. + expect(canvas.queryByRole("button", { name: /filters/i })).toBeNull(); + expect(canvas.getByLabelText("Filter")).toBeVisible(); + + // The picker trigger renders the committed preset label and opens + // to the quick-pick list with that preset selected. + await userEvent.click( + canvas.getByRole("button", { name: /Last 24 hours/ }), + ); + await waitFor(() => { + expect( + screen.getByRole("radio", { name: "Custom range" }), + ).toBeInTheDocument(); + }); + expect(screen.getByRole("radio", { name: "Last 24 hours" })).toBeChecked(); + }, }; export const WithQuery: Story = { @@ -81,8 +98,8 @@ export const ExplicitTimeRange: Story = { args: { ...defaultFilterProps, timeRange: { - startedAfter: new Date("2026-08-01T09:30:00"), - startedBefore: new Date("2026-08-02T17:45:00"), + start: new Date("2026-08-01T09:30:00"), + end: new Date("2026-08-02T17:45:00"), }, }, }; diff --git a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsFilter.tsx b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsFilter.tsx index 747064f7089..0198bfdaa41 100644 --- a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsFilter.tsx +++ b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsFilter.tsx @@ -1,6 +1,6 @@ import type { FC } from "react"; -import { DateTimeRangeFilter } from "#/components/DateTimeRangeFilter/DateTimeRangeFilter"; -import type { TimeRange } from "#/components/DateTimeRangeFilter/timeRange"; +import { DateTimeRangePicker } from "#/components/DateTimeRangePicker/DateTimeRangePicker"; +import type { DateTimeRangeValue } from "#/components/DateTimeRangePicker/dateTimeRange"; import { Filter, MenuSkeleton, @@ -27,9 +27,8 @@ interface ListSessionsFilterProps { client: ClientFilterMenu; model: ModelFilterMenu; }; - timeRange: TimeRange; - defaultTimeRange: TimeRange; - onTimeRangeChange: (range: TimeRange) => void; + timeRange: DateTimeRangeValue; + onTimeRangeChange: (value: DateTimeRangeValue) => void; } export const ListSessionsFilter: FC = ({ @@ -37,7 +36,6 @@ export const ListSessionsFilter: FC = ({ error, menus, timeRange, - defaultTimeRange, onTimeRangeChange, }) => { return ( @@ -45,24 +43,16 @@ export const ListSessionsFilter: FC = ({ filter={filter} optionsSkeleton={} isLoading={menus.user.isInitializing} - presets={[ - { - name: "All sessions", - query: "", - }, - { - name: "My sessions", - query: "initiator:me", - }, - ]} + // No preset queries: the dropdown menus and the range picker + // already cover them, so the Filters button would be redundant. + presets={[]} error={error} options={ <> - { const explicitTimeRange = parseTimeRange(filter.values); const timeRange = explicitTimeRange ?? defaultRange; + + // The URL stores only resolved timestamps; a preset is display metadata + // for the picker trigger. Remember the last committed picker value so the + // preset label survives re-renders, but show it only while the URL range + // still matches what the preset resolved to. The default window is the 24 + // hours ending at mount, which is exactly the "Last 24 hours" preset. + const [lastPicked, setLastPicked] = useState(() => ({ + start: defaultRange.startedAfter, + end: defaultRange.startedBefore, + preset: "last_24h", + })); + // RFC 3339 serialization truncates to seconds, so ranges that round-trip + // through the URL lose sub-second precision. Compare at second precision. + const sameSecond = (a: Date, b: Date) => + Math.floor(a.getTime() / 1000) === Math.floor(b.getTime() / 1000); + const preset = + lastPicked.preset !== undefined && + sameSecond(lastPicked.start, timeRange.startedAfter) && + sameSecond(lastPicked.end, timeRange.startedBefore) + ? lastPicked.preset + : undefined; + const userMenu = useUserFilterMenu({ value: filter.values.initiator, onChange: (option) => @@ -118,10 +141,20 @@ const AISessionListPage: FC = () => { client: clientMenu, model: modelMenu, }, - timeRange, - defaultTimeRange: defaultRange, - onTimeRangeChange: (range) => - filter.update(queryWithTimeRange(filter.values, range)), + timeRange: { + start: timeRange.startedAfter, + end: timeRange.startedBefore, + preset, + }, + onTimeRangeChange: (value) => { + setLastPicked(value); + filter.update( + queryWithTimeRange(filter.values, { + startedAfter: value.start, + startedBefore: value.end, + }), + ); + }, }} /> diff --git a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.stories.tsx b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.stories.tsx index 80e8e7a1d6a..e7db4c1e58a 100644 --- a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.stories.tsx +++ b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPageView.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; import type { ComponentProps } from "react"; import { fn } from "storybook/test"; -import type { TimeRange } from "#/components/DateTimeRangeFilter/timeRange"; +import type { DateTimeRangeValue } from "#/components/DateTimeRangePicker/dateTimeRange"; import { getDefaultFilterProps, MockMenu, @@ -15,9 +15,10 @@ import { ListSessionsPageView } from "./ListSessionsPageView"; type FilterProps = ComponentProps["filterProps"]; -const timeRange: TimeRange = { - startedAfter: new Date("2026-08-12T15:00:00Z"), - startedBefore: new Date("2026-08-13T15:00:00Z"), +const timeRange: DateTimeRangeValue = { + start: new Date("2026-08-12T15:00:00Z"), + end: new Date("2026-08-13T15:00:00Z"), + preset: "last_24h", }; const defaultFilterProps: FilterProps = { @@ -35,7 +36,6 @@ const defaultFilterProps: FilterProps = { }, }), timeRange, - defaultTimeRange: timeRange, onTimeRangeChange: fn(), }; From 1c09e43baea80c6debcbea42a72c25b4e9c4db94 Mon Sep 17 00:00:00 2001 From: Tracy Johnson Date: Thu, 20 Aug 2026 22:54:20 +0000 Subject: [PATCH 2/4] refactor(site/src/pages/AIBridgePage): tighten comments --- .../ListSessionsPage/ListSessionsFilter.tsx | 3 +-- .../AIBridgePage/ListSessionsPage/ListSessionsPage.tsx | 10 +++------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsFilter.tsx b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsFilter.tsx index 0198bfdaa41..d9f10c48ab9 100644 --- a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsFilter.tsx +++ b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsFilter.tsx @@ -43,8 +43,7 @@ export const ListSessionsFilter: FC = ({ filter={filter} optionsSkeleton={} isLoading={menus.user.isInitializing} - // No preset queries: the dropdown menus and the range picker - // already cover them, so the Filters button would be redundant. + // No preset queries; the search field and menus already cover them. presets={[]} error={error} options={ diff --git a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPage.tsx b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPage.tsx index 794b549dc35..b740cfab292 100644 --- a/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPage.tsx +++ b/site/src/pages/AIBridgePage/ListSessionsPage/ListSessionsPage.tsx @@ -61,18 +61,14 @@ const AISessionListPage: FC = () => { const explicitTimeRange = parseTimeRange(filter.values); const timeRange = explicitTimeRange ?? defaultRange; - // The URL stores only resolved timestamps; a preset is display metadata - // for the picker trigger. Remember the last committed picker value so the - // preset label survives re-renders, but show it only while the URL range - // still matches what the preset resolved to. The default window is the 24 - // hours ending at mount, which is exactly the "Last 24 hours" preset. + // The preset is display-only; the URL stores resolved timestamps. Show + // the preset label only while the URL range still matches it. const [lastPicked, setLastPicked] = useState(() => ({ start: defaultRange.startedAfter, end: defaultRange.startedBefore, preset: "last_24h", })); - // RFC 3339 serialization truncates to seconds, so ranges that round-trip - // through the URL lose sub-second precision. Compare at second precision. + // URL round-trips truncate to seconds, so compare at second precision. const sameSecond = (a: Date, b: Date) => Math.floor(a.getTime() / 1000) === Math.floor(b.getTime() / 1000); const preset = From afcd84a675872a6b3fcb5a2ed022b900dc0cb356 Mon Sep 17 00:00:00 2001 From: Tracy Johnson Date: Thu, 20 Aug 2026 22:58:43 +0000 Subject: [PATCH 3/4] fix(site/src/components/DateTimeRangePicker): rotate trigger chevron like other dropdowns Use the shared animated ChevronDownIcon so the trigger chevron flips while the popover is open, matching Select and Combobox triggers. --- .../components/DateTimeRangePicker/DateTimeRangePicker.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/site/src/components/DateTimeRangePicker/DateTimeRangePicker.tsx b/site/src/components/DateTimeRangePicker/DateTimeRangePicker.tsx index 20f75fa24de..0d97f72fb6c 100644 --- a/site/src/components/DateTimeRangePicker/DateTimeRangePicker.tsx +++ b/site/src/components/DateTimeRangePicker/DateTimeRangePicker.tsx @@ -3,9 +3,10 @@ * hidden until "Custom range" is chosen. */ -import { CalendarIcon, CheckIcon, ChevronDownIcon } from "lucide-react"; +import { CalendarIcon, CheckIcon } from "lucide-react"; import { type FC, type KeyboardEvent, useEffect, useId, useState } from "react"; import type { DateRange as DayPickerDateRange } from "react-day-picker"; +import { ChevronDownIcon } from "#/components/AnimatedIcons/ChevronDown"; import { Button, type ButtonProps } from "#/components/Button/Button"; import { Calendar } from "#/components/Calendar/Calendar"; import { Input } from "#/components/Input/Input"; @@ -210,7 +211,7 @@ export const DateTimeRangePicker: FC = ({ return ( -