From c07e1dc7a61ab86feb6abc70f3758cf27764e887 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 02:20:42 +0000 Subject: [PATCH] fix(ui): search all users from the User Usage filter box Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../components/EntityUsage/EntityUsage.tsx | 9 ++- .../_components/components/UsagePageView.tsx | 7 +++ .../UsageExportHeader.test.tsx | 58 ++++++++++++++++++- .../EntityUsageExport/UsageExportHeader.tsx | 55 +++++++++++++----- .../src/components/EntityUsageExport/types.ts | 8 +++ 5 files changed, 118 insertions(+), 19 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx index 2d6dbb823cc..27c0826d880 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx @@ -25,7 +25,7 @@ import React, { type ReactNode, useMemo, useState } from "react"; import TeamMultiSelect from "@/components/common_components/team_multi_select"; import { ActivityMetrics, processActivityData } from "@/components/activity_metrics"; import { UsageExportHeader } from "@/components/EntityUsageExport"; -import type { EntityType } from "@/components/EntityUsageExport/types"; +import type { EntityType, FilterSearch } from "@/components/EntityUsageExport/types"; import { agentDailyActivityCall, customerDailyActivityCall, @@ -84,6 +84,7 @@ interface EntityUsageProps { entityList: EntityList[] | null; premiumUser: boolean; dateValue: DateRangePickerValue; + filterSearch?: FilterSearch; } const ENTITY_FETCH_FNS: Record Promise> = { @@ -107,6 +108,7 @@ const EntityUsage: React.FC = ({ entityList, userRole, dateValue, + filterSearch, }) => { const { teams } = useTeams(); const [selectedTags, setSelectedTags] = useState([]); @@ -685,13 +687,16 @@ const EntityUsage: React.FC = ({ dateValue={dateValue} entityType={entityType} spendData={spendData} - showFilters={entityType !== "team" && entityList !== null && entityList.length > 0} + showFilters={ + entityType !== "team" && (filterSearch !== undefined || (entityList !== null && entityList.length > 0)) + } filterLabel={getFilterLabel(entityType)} filterPlaceholder={getFilterPlaceholder(entityType)} selectedFilters={selectedTags} onFiltersChange={setSelectedTags} filterOptions={getAllTags() || undefined} filterMode={entityType === "user" ? "single" : "multiple"} + filterSearch={filterSearch} teams={teams || []} /> diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx index a716d59bdc4..4eeff926ad7 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx @@ -1045,6 +1045,13 @@ const UsagePage: React.FC = ({ teams, organizations }) => { entityList={userOptions.length > 0 ? userOptions : null} premiumUser={premiumUser} dateValue={dateValue} + filterSearch={{ + onSearchChange: setSettledUserSearch, + onLoadMore: fetchNextUsersPage, + hasNextPage: hasNextUsersPage, + isLoading: isLoadingUsers, + isFetchingNextPage: isFetchingNextUsersPage, + }} /> )} {/* User Agent Activity Panel */} diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/UsageExportHeader.test.tsx b/ui/litellm-dashboard/src/components/EntityUsageExport/UsageExportHeader.test.tsx index 729d6fd3406..42dadc33e1e 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/UsageExportHeader.test.tsx +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/UsageExportHeader.test.tsx @@ -1,4 +1,4 @@ -import { renderWithProviders, screen } from "../../../tests/test-utils"; +import { renderWithProviders, screen, waitFor } from "../../../tests/test-utils"; import userEvent from "@testing-library/user-event"; import { vi } from "vitest"; import UsageExportHeader from "./UsageExportHeader"; @@ -70,4 +70,60 @@ describe("UsageExportHeader", () => { ); expect(screen.getByText("Team")).toBeInTheDocument(); }); + + describe("single-select filter with server-side search", () => { + const searchProps = { + ...defaultProps, + entityType: "user" as const, + showFilters: true, + filterMode: "single" as const, + filterLabel: "Filter by user", + filterPlaceholder: "Select user to filter...", + }; + + it("should report the typed query so users outside the loaded page can be found", async () => { + const user = userEvent.setup(); + const onSearchChange = vi.fn(); + renderWithProviders( + , + ); + + const input = screen.getByRole("combobox"); + await user.click(input); + await user.type(input, "alpha10"); + + expect(input).toHaveValue("alpha10"); + await waitFor(() => expect(onSearchChange).toHaveBeenCalledWith("alpha10")); + }); + + it("should keep the filter mounted when a search returns no loaded options", () => { + renderWithProviders( + , + ); + + expect(screen.getByRole("combobox")).toBeInTheDocument(); + }); + }); }); diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/UsageExportHeader.tsx b/ui/litellm-dashboard/src/components/EntityUsageExport/UsageExportHeader.tsx index 78781df5948..93c3ffc3766 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/UsageExportHeader.tsx +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/UsageExportHeader.tsx @@ -15,8 +15,9 @@ import { ComboboxList, ComboboxValue, } from "@/components/ui/combobox"; +import { PaginatedSearchSelect } from "@/components/shared/PaginatedSearchSelect"; import EntityUsageExportModal from "./EntityUsageExportModal"; -import type { EntitySpendData, EntityType } from "./types"; +import type { EntitySpendData, EntityType, FilterSearch } from "./types"; import type { Team } from "@/components/key_team_helpers/key_list"; interface UsageExportHeaderProps { @@ -31,6 +32,7 @@ interface UsageExportHeaderProps { onFiltersChange?: (filters: string[]) => void; filterOptions?: Array<{ label: string; value: string }>; filterMode?: "multiple" | "single"; + filterSearch?: FilterSearch; customTitle?: string; compactLayout?: boolean; teams?: Team[]; @@ -47,13 +49,14 @@ const UsageExportHeader: React.FC = ({ onFiltersChange, filterOptions = [], filterMode = "multiple", + filterSearch, customTitle, compactLayout = false, teams = [], }) => { const [isExportModalOpen, setIsExportModalOpen] = useState(false); - const hasFilters = showFilters && filterOptions.length > 0; + const hasFilters = showFilters && (filterOptions.length > 0 || filterSearch !== undefined); const optionValues = filterOptions.map((option) => option.value); const labelOf = (value: string) => filterOptions.find((option) => option.value === value)?.label ?? value; @@ -70,6 +73,39 @@ const UsageExportHeader: React.FC = ({ ); + const searchableSingleSelect = + filterSearch === undefined ? null : ( + onFiltersChange?.(next === "" ? [] : [next])} + onSearchChange={filterSearch.onSearchChange} + onLoadMore={filterSearch.onLoadMore} + hasNextPage={filterSearch.hasNextPage} + isLoading={filterSearch.isLoading} + isFetchingNextPage={filterSearch.isFetchingNextPage} + placeholder={filterPlaceholder} + emptyText="No options found" + /> + ); + + const singleSelect = searchableSingleSelect ?? ( + onFiltersChange?.(next ? [next] : [])} + itemToStringLabel={labelOf} + > + 0} + /> + {filterList} + + ); + return ( <>
@@ -83,20 +119,7 @@ const UsageExportHeader: React.FC = ({
{filterLabel && } {filterMode === "single" ? ( - onFiltersChange?.(next ? [next] : [])} - itemToStringLabel={labelOf} - > - 0} - /> - {filterList} - + singleSelect ) : ( void; + onLoadMore: () => void; + hasNextPage: boolean; + isLoading: boolean; + isFetchingNextPage: boolean; +} + export interface EntityUsageExportModalProps { isOpen: boolean; onClose: () => void;