feat(site/src): use DateTimeRangePicker on the AI sessions page (#28426)

Follow-up to #28392. Wires the new `DateTimeRangePicker` into the AI
Gateway sessions page, replacing the text-expression
`DateTimeRangeFilter`, and removes the redundant "Filters" preset
button.

- The picker trigger sits in the filter row alongside the search field
and the User/Provider/Client/Model dropdowns.
- The preset queries (All sessions, My sessions) behind the Filters
button duplicate the search field and the user dropdown, so the button
is dropped by passing no presets. Other pages using the shared `Filter`
component are unaffected.
- No API or URL contract changes: the filter query still stores resolved
`started_after`/`started_before` timestamps. The picker's preset id is
display-only component state, shown only while the URL range still
matches what the preset resolved to, so shared links keep rendering as
custom ranges.
- Clearing the search resets the picker to the default window, labeled
"Last 24 hours".
- The picker trigger chevron now uses the shared animated
`ChevronDownIcon`, rotating on open like the other dropdown triggers.
- The old `DateTimeRangeFilter` component is left in place (still
referenced by its stories); removing it can be a separate cleanup.

Storybook: the sessions filter story now asserts the Filters button is
gone and exercises opening the picker; existing stories updated to the
new value shape. `biome`, `tsc`, and the Storybook interaction tests for
the page pass.

---

Generated by Coder Agents on behalf of @tracyjohnsonux.
This commit is contained in:
TJ
2026-08-20 23:08:00 +00:00
committed by GitHub
parent 952c0ce5e6
commit 1c139402d0
5 changed files with 78 additions and 42 deletions
@@ -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<DateTimeRangePickerProps> = ({
return (
<Popover open={open} onOpenChange={handleOpenChange}>
<PopoverTrigger asChild>
<Button variant="outline" size={size}>
<Button variant="outline" size={size} className="group">
<CalendarIcon className="size-4 text-content-secondary" />
<span>{triggerLabel}</span>
<ChevronDownIcon className="size-3.5 text-content-secondary" />
@@ -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<typeof ListSessionsFilter>;
type FilterAndMenus = Pick<FilterProps, "filter" | "menus">;
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<FilterProps, "timeRange" | "onTimeRangeChange"> = {
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"),
},
},
};
@@ -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<ListSessionsFilterProps> = ({
@@ -37,7 +36,6 @@ export const ListSessionsFilter: FC<ListSessionsFilterProps> = ({
error,
menus,
timeRange,
defaultTimeRange,
onTimeRangeChange,
}) => {
return (
@@ -45,24 +43,15 @@ export const ListSessionsFilter: FC<ListSessionsFilterProps> = ({
filter={filter}
optionsSkeleton={<MenuSkeleton />}
isLoading={menus.user.isInitializing}
presets={[
{
name: "All sessions",
query: "",
},
{
name: "My sessions",
query: "initiator:me",
},
]}
// No preset queries; the search field and menus already cover them.
presets={[]}
error={error}
options={
<>
<DateTimeRangeFilter
<DateTimeRangePicker
value={timeRange}
defaultValue={defaultTimeRange}
onChange={onTimeRangeChange}
width={FILTER_WIDTH}
size="lg"
/>
<UserMenu
menu={menus.user}
@@ -2,6 +2,7 @@ import type { FC } from "react";
import { useState } from "react";
import { useNavigate, useSearchParams } from "react-router";
import { paginatedSessions } from "#/api/queries/aiBridge";
import type { DateTimeRangeValue } from "#/components/DateTimeRangePicker/dateTimeRange";
import { useFilter, useFilterParamsKey } from "#/components/Filter/Filter";
import { useUserFilterMenu } from "#/components/Filter/UserFilter";
import { useAuthenticated } from "#/hooks/useAuthenticated";
@@ -59,6 +60,24 @@ const AISessionListPage: FC = () => {
const explicitTimeRange = parseTimeRange(filter.values);
const timeRange = explicitTimeRange ?? defaultRange;
// The preset is display-only; the URL stores resolved timestamps. With no
// range in the URL (e.g. after clearing the search) the default window
// applies, which is the "Last 24 hours" preset. Otherwise show the last
// picked preset only while the URL range still matches it.
const [lastPicked, setLastPicked] = useState<DateTimeRangeValue | null>(null);
// 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 =
explicitTimeRange === null
? "last_24h"
: 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 +137,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,
}),
);
},
}}
/>
</RequirePermission>
@@ -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<typeof ListSessionsPageView>["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(),
};