mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
feat: default AI Gateway sessions list to a 24h time range (#28256)
The AI Gateway sessions list page takes 5-13 seconds to load because `ListAIBridgeSessions` scans all `aibridge_interceptions` rows (~1M on dogfood) when no time filter is set. This PR defaults the list to the last 24 hours of sessions, reducing the scan by roughly two orders of magnitude without any backend or migration changes: the `started_after`/`started_before` filters already exist end-to-end in SQL, searchquery, and the API. The default range is held in component state (not the URL) and merged into every query payload including prefetches, so the unbounded query never runs on page load. A time range filter in the filter bar lets users override the window explicitly, which doubles as a forensic tool. Picking a range writes quoted RFC 3339 timestamps into the existing filter query. There are no presets and no unbounded "all time" mode, so the fast path is the only path. Existing "All sessions"/"My sessions" presets reset the filter query, which resets the time window to the default 24 hours; that is intentional. Also makes the five filter triggers a uniform width and left-aligns the new picker so the search input keeps room on wide viewports. Depends on #28255 (the DateTimeRangeFilter component) and #28254 (filterQuery serialization). 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:
@@ -46,9 +46,13 @@ not just what was called.
|
||||
|
||||
### Sessions list
|
||||
|
||||
The sessions page (`http://<deployment-url>/ai-gateway/sessions`) lists all sessions in
|
||||
reverse-chronological order. Each row shows the last prompt, initiator, provider,
|
||||
client, token usage, network requests, thread count, and timestamp.
|
||||
The sessions page (`http://<deployment-url>/ai-gateway/sessions`) lists sessions in
|
||||
reverse-chronological order. By default it shows sessions with activity in the
|
||||
last 24 hours. Use the time range filter in the filter bar to widen or narrow
|
||||
the window, for example to see older sessions.
|
||||
|
||||
Each row shows the last prompt, initiator, provider, client, token usage,
|
||||
network requests, thread count, and timestamp.
|
||||
|
||||
The **Network Requests** column reports the total and blocked
|
||||
[Agent Firewall](../agent-firewall/index.md) requests for the session. It shows
|
||||
|
||||
@@ -1,5 +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 {
|
||||
getDefaultFilterProps,
|
||||
MockMenu,
|
||||
@@ -8,21 +10,38 @@ import { ListSessionsFilter } from "./ListSessionsFilter";
|
||||
|
||||
type FilterProps = ComponentProps<typeof ListSessionsFilter>;
|
||||
|
||||
const defaultFilterProps = getDefaultFilterProps<
|
||||
Pick<FilterProps, "filter" | "menus">
|
||||
>({
|
||||
query: "",
|
||||
values: {
|
||||
username: undefined,
|
||||
provider: undefined,
|
||||
},
|
||||
menus: {
|
||||
user: MockMenu,
|
||||
provider: MockMenu,
|
||||
client: MockMenu,
|
||||
model: MockMenu,
|
||||
},
|
||||
});
|
||||
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 timeRangeProps: Pick<
|
||||
FilterProps,
|
||||
"timeRange" | "defaultTimeRange" | "onTimeRangeChange"
|
||||
> = {
|
||||
timeRange,
|
||||
defaultTimeRange: timeRange,
|
||||
onTimeRangeChange: fn(),
|
||||
};
|
||||
|
||||
const defaultFilterProps = {
|
||||
...getDefaultFilterProps<FilterAndMenus>({
|
||||
query: "",
|
||||
values: {
|
||||
username: undefined,
|
||||
provider: undefined,
|
||||
},
|
||||
menus: {
|
||||
user: MockMenu,
|
||||
provider: MockMenu,
|
||||
client: MockMenu,
|
||||
model: MockMenu,
|
||||
},
|
||||
}),
|
||||
...timeRangeProps,
|
||||
};
|
||||
|
||||
const meta: Meta<typeof ListSessionsFilter> = {
|
||||
title: "pages/AIBridgePage/ListSessionsFilter",
|
||||
@@ -40,7 +59,7 @@ export const Default: Story = {
|
||||
|
||||
export const WithQuery: Story = {
|
||||
args: {
|
||||
...getDefaultFilterProps<Pick<FilterProps, "filter" | "menus">>({
|
||||
...getDefaultFilterProps<FilterAndMenus>({
|
||||
query: "initiator:me",
|
||||
values: {
|
||||
username: "me",
|
||||
@@ -54,6 +73,17 @@ export const WithQuery: Story = {
|
||||
},
|
||||
used: true,
|
||||
}),
|
||||
...timeRangeProps,
|
||||
},
|
||||
};
|
||||
|
||||
export const ExplicitTimeRange: Story = {
|
||||
args: {
|
||||
...defaultFilterProps,
|
||||
timeRange: {
|
||||
startedAfter: new Date("2026-08-01T09:30:00"),
|
||||
startedBefore: new Date("2026-08-02T17:45:00"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { FC } from "react";
|
||||
import { DateTimeRangeFilter } from "#/components/DateTimeRangeFilter/DateTimeRangeFilter";
|
||||
import type { TimeRange } from "#/components/DateTimeRangeFilter/timeRange";
|
||||
import {
|
||||
Filter,
|
||||
MenuSkeleton,
|
||||
@@ -12,6 +14,10 @@ import {
|
||||
type ProviderFilterMenu,
|
||||
} from "../filters/ProviderFilter";
|
||||
|
||||
// Narrower than the SelectFilter default so the search input keeps most
|
||||
// of the row on wide viewports.
|
||||
const FILTER_WIDTH = 150;
|
||||
|
||||
interface ListSessionsFilterProps {
|
||||
filter: ReturnType<typeof useFilter>;
|
||||
error?: unknown;
|
||||
@@ -21,12 +27,18 @@ interface ListSessionsFilterProps {
|
||||
client: ClientFilterMenu;
|
||||
model: ModelFilterMenu;
|
||||
};
|
||||
timeRange: TimeRange;
|
||||
defaultTimeRange: TimeRange;
|
||||
onTimeRangeChange: (range: TimeRange) => void;
|
||||
}
|
||||
|
||||
export const ListSessionsFilter: FC<ListSessionsFilterProps> = ({
|
||||
filter,
|
||||
error,
|
||||
menus,
|
||||
timeRange,
|
||||
defaultTimeRange,
|
||||
onTimeRangeChange,
|
||||
}) => {
|
||||
return (
|
||||
<Filter
|
||||
@@ -46,10 +58,20 @@ export const ListSessionsFilter: FC<ListSessionsFilterProps> = ({
|
||||
error={error}
|
||||
options={
|
||||
<>
|
||||
<UserMenu menu={menus.user} placeholder="All users" />
|
||||
<ProviderFilter menu={menus.provider} />
|
||||
<ClientFilter menu={menus.client} />
|
||||
<ModelFilter menu={menus.model} />
|
||||
<DateTimeRangeFilter
|
||||
value={timeRange}
|
||||
defaultValue={defaultTimeRange}
|
||||
onChange={onTimeRangeChange}
|
||||
width={FILTER_WIDTH}
|
||||
/>
|
||||
<UserMenu
|
||||
menu={menus.user}
|
||||
placeholder="All users"
|
||||
width={FILTER_WIDTH}
|
||||
/>
|
||||
<ProviderFilter menu={menus.provider} width={FILTER_WIDTH} />
|
||||
<ClientFilter menu={menus.client} width={FILTER_WIDTH} />
|
||||
<ModelFilter menu={menus.model} width={FILTER_WIDTH} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { FC } from "react";
|
||||
import { useState } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router";
|
||||
import { paginatedSessions } from "#/api/queries/aiBridge";
|
||||
import { useFilter } from "#/components/Filter/Filter";
|
||||
import { useFilter, useFilterParamsKey } from "#/components/Filter/Filter";
|
||||
import { useUserFilterMenu } from "#/components/Filter/UserFilter";
|
||||
import { useAuthenticated } from "#/hooks/useAuthenticated";
|
||||
import { usePaginatedQuery } from "#/hooks/usePaginatedQuery";
|
||||
@@ -13,6 +14,12 @@ import { useModelFilterMenu } from "../filters/ModelFilter";
|
||||
import { useProviderFilterMenu } from "../filters/ProviderFilter";
|
||||
import { getAIBridgePermissions } from "../getAIBridgePermissions";
|
||||
import { ListSessionsPageView } from "./ListSessionsPageView";
|
||||
import {
|
||||
defaultTimeRange,
|
||||
parseTimeRange,
|
||||
queryWithTimeRange,
|
||||
withDefaultTimeRange,
|
||||
} from "./timeRange";
|
||||
|
||||
const AISessionListPage: FC = () => {
|
||||
const { permissions } = useAuthenticated();
|
||||
@@ -26,9 +33,21 @@ const AISessionListPage: FC = () => {
|
||||
|
||||
const canViewSessions = isEntitled && hasPermission;
|
||||
|
||||
// The default time range lives in memory, not the URL, so a shared link
|
||||
// resolves relative to the viewer's current time. It is fixed per mount
|
||||
// so query cache keys stay stable.
|
||||
const [defaultRange] = useState(() => defaultTimeRange(new Date()));
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const sessionsQuery = usePaginatedQuery({
|
||||
...paginatedSessions(searchParams),
|
||||
// Merge the default range into every fetch (including prefetches) so
|
||||
// the unfiltered sessions query never scans the entire table.
|
||||
queryPayload: () =>
|
||||
withDefaultTimeRange(
|
||||
searchParams.get(useFilterParamsKey) ?? "",
|
||||
defaultRange,
|
||||
),
|
||||
enabled: canViewSessions,
|
||||
});
|
||||
|
||||
@@ -38,6 +57,8 @@ const AISessionListPage: FC = () => {
|
||||
onUpdate: sessionsQuery.goToFirstPage,
|
||||
});
|
||||
|
||||
const explicitTimeRange = parseTimeRange(filter.values);
|
||||
const timeRange = explicitTimeRange ?? defaultRange;
|
||||
const userMenu = useUserFilterMenu({
|
||||
value: filter.values.initiator,
|
||||
onChange: (option) =>
|
||||
@@ -97,6 +118,10 @@ const AISessionListPage: FC = () => {
|
||||
client: clientMenu,
|
||||
model: modelMenu,
|
||||
},
|
||||
timeRange,
|
||||
defaultTimeRange: defaultRange,
|
||||
onTimeRangeChange: (range) =>
|
||||
filter.update(queryWithTimeRange(filter.values, range)),
|
||||
}}
|
||||
/>
|
||||
</RequirePermission>
|
||||
|
||||
@@ -1,6 +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 {
|
||||
getDefaultFilterProps,
|
||||
MockMenu,
|
||||
@@ -14,19 +15,29 @@ import { ListSessionsPageView } from "./ListSessionsPageView";
|
||||
|
||||
type FilterProps = ComponentProps<typeof ListSessionsPageView>["filterProps"];
|
||||
|
||||
const defaultFilterProps = getDefaultFilterProps<FilterProps>({
|
||||
query: "owner:me",
|
||||
values: {
|
||||
username: undefined,
|
||||
provider: undefined,
|
||||
},
|
||||
menus: {
|
||||
user: MockMenu,
|
||||
provider: MockMenu,
|
||||
client: MockMenu,
|
||||
model: MockMenu,
|
||||
},
|
||||
});
|
||||
const timeRange: TimeRange = {
|
||||
startedAfter: new Date("2026-08-12T15:00:00Z"),
|
||||
startedBefore: new Date("2026-08-13T15:00:00Z"),
|
||||
};
|
||||
|
||||
const defaultFilterProps: FilterProps = {
|
||||
...getDefaultFilterProps<FilterProps>({
|
||||
query: "owner:me",
|
||||
values: {
|
||||
username: undefined,
|
||||
provider: undefined,
|
||||
},
|
||||
menus: {
|
||||
user: MockMenu,
|
||||
provider: MockMenu,
|
||||
client: MockMenu,
|
||||
model: MockMenu,
|
||||
},
|
||||
}),
|
||||
timeRange,
|
||||
defaultTimeRange: timeRange,
|
||||
onTimeRangeChange: fn(),
|
||||
};
|
||||
|
||||
const meta: Meta<typeof ListSessionsPageView> = {
|
||||
title: "pages/AIBridgePage/ListSessionsPageView",
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { TimeRange } from "#/components/DateTimeRangeFilter/timeRange";
|
||||
import {
|
||||
defaultTimeRange,
|
||||
parseTimeRange,
|
||||
queryWithTimeRange,
|
||||
toRFC3339,
|
||||
withDefaultTimeRange,
|
||||
} from "./timeRange";
|
||||
|
||||
const now = new Date(2026, 7, 13, 15, 0, 0);
|
||||
|
||||
describe("toRFC3339", () => {
|
||||
it("serializes in UTC with second precision", () => {
|
||||
expect(toRFC3339(new Date(Date.UTC(2026, 7, 13, 10, 0, 0, 500)))).toBe(
|
||||
"2026-08-13T10:00:00Z",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("defaultTimeRange", () => {
|
||||
it("spans the 24 hours ending at now", () => {
|
||||
const range = defaultTimeRange(now);
|
||||
expect(range.startedBefore).toEqual(now);
|
||||
expect(range.startedAfter).toEqual(
|
||||
new Date(now.getTime() - 24 * 60 * 60 * 1000),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("withDefaultTimeRange", () => {
|
||||
const range: TimeRange = {
|
||||
startedAfter: new Date(Date.UTC(2026, 7, 12, 15, 0, 0)),
|
||||
startedBefore: new Date(Date.UTC(2026, 7, 13, 15, 0, 0)),
|
||||
};
|
||||
|
||||
it("appends the default range to an empty query", () => {
|
||||
expect(withDefaultTimeRange("", range)).toBe(
|
||||
'started_after:"2026-08-12T15:00:00Z" started_before:"2026-08-13T15:00:00Z"',
|
||||
);
|
||||
});
|
||||
|
||||
it("appends the default range alongside other filters", () => {
|
||||
expect(withDefaultTimeRange("initiator:me", range)).toBe(
|
||||
'initiator:me started_after:"2026-08-12T15:00:00Z" started_before:"2026-08-13T15:00:00Z"',
|
||||
);
|
||||
});
|
||||
|
||||
it("leaves a query with an explicit time range untouched", () => {
|
||||
const afterOnly = 'started_after:"2026-08-01T00:00:00Z"';
|
||||
expect(withDefaultTimeRange(afterOnly, range)).toBe(afterOnly);
|
||||
|
||||
const beforeOnly = 'started_before:"2026-08-01T00:00:00Z"';
|
||||
expect(withDefaultTimeRange(beforeOnly, range)).toBe(beforeOnly);
|
||||
});
|
||||
|
||||
it("does not mistake a quoted value containing the key for a bound", () => {
|
||||
// A quoted value that mentions started_after is not a time bound.
|
||||
const query = 'session_id:"started_after:2026-08-01"';
|
||||
expect(withDefaultTimeRange(query, range)).toBe(
|
||||
'session_id:"started_after:2026-08-01" started_after:"2026-08-12T15:00:00Z" started_before:"2026-08-13T15:00:00Z"',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("queryWithTimeRange", () => {
|
||||
const range: TimeRange = {
|
||||
startedAfter: new Date(Date.UTC(2026, 7, 12, 15, 0, 0)),
|
||||
startedBefore: new Date(Date.UTC(2026, 7, 13, 15, 0, 0)),
|
||||
};
|
||||
|
||||
it("preserves other filters and replaces the time range", () => {
|
||||
expect(
|
||||
queryWithTimeRange(
|
||||
{
|
||||
initiator: "me",
|
||||
started_after: "2026-01-01T00:00:00Z",
|
||||
started_before: "2026-01-02T00:00:00Z",
|
||||
},
|
||||
range,
|
||||
),
|
||||
).toBe(
|
||||
'initiator:me started_after:"2026-08-12T15:00:00Z" started_before:"2026-08-13T15:00:00Z"',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseTimeRange", () => {
|
||||
it("parses an explicit range", () => {
|
||||
expect(
|
||||
parseTimeRange({
|
||||
started_after: "2026-08-12T15:00:00Z",
|
||||
started_before: "2026-08-13T15:00:00Z",
|
||||
}),
|
||||
).toEqual({
|
||||
startedAfter: new Date(Date.UTC(2026, 7, 12, 15, 0, 0)),
|
||||
startedBefore: new Date(Date.UTC(2026, 7, 13, 15, 0, 0)),
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null when either bound is missing", () => {
|
||||
expect(parseTimeRange({})).toBeNull();
|
||||
expect(
|
||||
parseTimeRange({ started_after: "2026-08-12T15:00:00Z" }),
|
||||
).toBeNull();
|
||||
expect(
|
||||
parseTimeRange({ started_before: "2026-08-13T15:00:00Z" }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for malformed bounds", () => {
|
||||
expect(
|
||||
parseTimeRange({
|
||||
started_after: "bogus",
|
||||
started_before: "2026-08-13T15:00:00Z",
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import dayjs from "dayjs";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import type { TimeRange } from "#/components/DateTimeRangeFilter/timeRange";
|
||||
import {
|
||||
parseFilterQuery,
|
||||
stringifyFilter,
|
||||
} from "#/components/Filter/filterQuery";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
/** Serializes a Date as RFC 3339 in UTC with second precision. */
|
||||
export const toRFC3339 = (date: Date): string => {
|
||||
return dayjs(date).utc().format("YYYY-MM-DDTHH:mm:ss[Z]");
|
||||
};
|
||||
|
||||
/** The default sessions window: the 24 hours ending at now. */
|
||||
export const defaultTimeRange = (now: Date): TimeRange => ({
|
||||
startedAfter: new Date(now.getTime() - 24 * 60 * 60 * 1000),
|
||||
startedBefore: now,
|
||||
});
|
||||
|
||||
/**
|
||||
* Appends the default time range to a filter query unless the query already
|
||||
* sets started_after or started_before. A deliberately one-sided query is
|
||||
* left alone. The default is kept in memory instead of the URL so shared
|
||||
* links resolve relative to the viewer's current time.
|
||||
*/
|
||||
export const withDefaultTimeRange = (
|
||||
query: string,
|
||||
range: TimeRange,
|
||||
): string => {
|
||||
const values = parseFilterQuery(query);
|
||||
if (
|
||||
values.started_after !== undefined ||
|
||||
values.started_before !== undefined
|
||||
) {
|
||||
return query;
|
||||
}
|
||||
const suffix = stringifyFilter({
|
||||
...values,
|
||||
started_after: toRFC3339(range.startedAfter),
|
||||
started_before: toRFC3339(range.startedBefore),
|
||||
});
|
||||
return suffix;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a filter query string that replaces any existing time range in
|
||||
* values with the given range while preserving all other filters.
|
||||
* stringifyFilter quotes the RFC 3339 values because the backend query
|
||||
* parser treats unquoted colons as key/value separators.
|
||||
*/
|
||||
export const queryWithTimeRange = (
|
||||
values: Record<string, string | undefined>,
|
||||
range: TimeRange,
|
||||
): string => {
|
||||
return stringifyFilter({
|
||||
...values,
|
||||
started_after: toRFC3339(range.startedAfter),
|
||||
started_before: toRFC3339(range.startedBefore),
|
||||
});
|
||||
};
|
||||
|
||||
/** Extracts an explicit time range from filter values, or null if absent. */
|
||||
export const parseTimeRange = (
|
||||
values: Record<string, string | undefined>,
|
||||
): TimeRange | null => {
|
||||
const after = values.started_after;
|
||||
const before = values.started_before;
|
||||
if (!after || !before) {
|
||||
return null;
|
||||
}
|
||||
const startedAfter = new Date(after);
|
||||
const startedBefore = new Date(before);
|
||||
if (
|
||||
Number.isNaN(startedAfter.getTime()) ||
|
||||
Number.isNaN(startedBefore.getTime())
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return { startedAfter, startedBefore };
|
||||
};
|
||||
@@ -56,9 +56,10 @@ export type ClientFilterMenu = ReturnType<typeof useClientFilterMenu>;
|
||||
|
||||
interface ClientFilterProps {
|
||||
menu: ClientFilterMenu;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export const ClientFilter: React.FC<ClientFilterProps> = ({ menu }) => {
|
||||
export const ClientFilter: React.FC<ClientFilterProps> = ({ menu, width }) => {
|
||||
return (
|
||||
<SelectFilter
|
||||
label="Select client"
|
||||
@@ -67,6 +68,7 @@ export const ClientFilter: React.FC<ClientFilterProps> = ({ menu }) => {
|
||||
options={menu.searchOptions}
|
||||
onSelect={(option) => menu.selectOption(option)}
|
||||
selectedOption={menu.selectedOption ?? undefined}
|
||||
width={width}
|
||||
selectFilterSearch={
|
||||
<ComboboxInput
|
||||
placeholder="Search client..."
|
||||
|
||||
@@ -55,9 +55,10 @@ export type ModelFilterMenu = ReturnType<typeof useModelFilterMenu>;
|
||||
|
||||
interface ModelFilterProps {
|
||||
menu: ModelFilterMenu;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export const ModelFilter: FC<ModelFilterProps> = ({ menu }) => {
|
||||
export const ModelFilter: FC<ModelFilterProps> = ({ menu, width }) => {
|
||||
return (
|
||||
<SelectFilter
|
||||
label="Select model"
|
||||
@@ -66,6 +67,7 @@ export const ModelFilter: FC<ModelFilterProps> = ({ menu }) => {
|
||||
options={menu.searchOptions}
|
||||
onSelect={(option) => menu.selectOption(option)}
|
||||
selectedOption={menu.selectedOption ?? undefined}
|
||||
width={width}
|
||||
selectFilterSearch={
|
||||
<ComboboxInput
|
||||
placeholder="Search model..."
|
||||
|
||||
@@ -48,9 +48,10 @@ export type ProviderFilterMenu = ReturnType<typeof useProviderFilterMenu>;
|
||||
|
||||
interface ProviderFilterProps {
|
||||
menu: ProviderFilterMenu;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export const ProviderFilter: FC<ProviderFilterProps> = ({ menu }) => {
|
||||
export const ProviderFilter: FC<ProviderFilterProps> = ({ menu, width }) => {
|
||||
return (
|
||||
<SelectFilter
|
||||
label="Select provider"
|
||||
@@ -59,6 +60,7 @@ export const ProviderFilter: FC<ProviderFilterProps> = ({ menu }) => {
|
||||
options={menu.searchOptions}
|
||||
onSelect={(option) => menu.selectOption(option)}
|
||||
selectedOption={menu.selectedOption ?? undefined}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user