mirror of
https://github.com/coder/coder.git
synced 2026-10-31 02:43:21 +08:00
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:
@@ -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(),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user