diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.test.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.test.tsx
index dc201cccfeaf..1ae285f7e605 100644
--- a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.test.tsx
+++ b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.test.tsx
@@ -49,6 +49,10 @@ vi.mock("../../../common_components/team_multi_select", () => ({
default: () =>
Team Multi Select
,
}));
+vi.mock("../../../common_components/user_single_select", () => ({
+ default: () => User Single Select
,
+}));
+
// Mock useTeams hook
vi.mock("@/app/(dashboard)/hooks/useTeams", () => ({
default: vi.fn(() => ({
diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx
index 9fa73dd1728b..b55900c67847 100644
--- a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx
+++ b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx
@@ -26,6 +26,7 @@ import { ExportOutlined, LoadingOutlined } from "@ant-design/icons";
import { Alert, Button } from "antd";
import React, { useMemo, useState } from "react";
import TeamMultiSelect from "../../../common_components/team_multi_select";
+import UserSingleSelect from "../../../common_components/user_single_select";
import { ActivityMetrics, processActivityData } from "../../../activity_metrics";
import { UsageExportHeader } from "../../../EntityUsageExport";
import type { EntityType } from "../../../EntityUsageExport/types";
@@ -478,11 +479,20 @@ const EntityUsage: React.FC = ({ accessToken, entityType, enti
/>
)}
+ {entityType === "user" && (
+
+ Filter by user
+ setSelectedTags(value ? [value] : [])}
+ />
+
+ )}
0}
+ showFilters={entityType !== "team" && entityType !== "user" && entityList !== null && entityList.length > 0}
filterLabel={getFilterLabel(entityType)}
filterPlaceholder={getFilterPlaceholder(entityType)}
selectedFilters={selectedTags}
diff --git a/ui/litellm-dashboard/src/components/common_components/user_single_select.test.tsx b/ui/litellm-dashboard/src/components/common_components/user_single_select.test.tsx
new file mode 100644
index 000000000000..34530191885f
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/common_components/user_single_select.test.tsx
@@ -0,0 +1,152 @@
+import { act, render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, it, vi } from "vitest";
+import UserSingleSelect from "./user_single_select";
+
+const mockUseInfiniteUsers = vi.fn();
+
+vi.mock("@/app/(dashboard)/hooks/users/useUsers", () => ({
+ useInfiniteUsers: (...args: any[]) => mockUseInfiniteUsers(...args),
+}));
+
+const buildPage = (users: Array<{ user_id: string; user_email: string | null; user_alias: string | null }>) => ({
+ users,
+ page: 1,
+ page_size: 50,
+ total: users.length,
+ total_pages: 1,
+});
+
+const DEFAULT_HOOK_STATE = {
+ data: {
+ pages: [
+ buildPage([
+ { user_id: "user-1", user_email: "alice@example.com", user_alias: null },
+ { user_id: "user-2", user_email: null, user_alias: "Bob" },
+ { user_id: "user-3", user_email: "charlie@example.com", user_alias: "Charlie" },
+ ]),
+ ],
+ },
+ fetchNextPage: vi.fn(),
+ hasNextPage: false,
+ isFetchingNextPage: false,
+ isLoading: false,
+};
+
+describe("UserSingleSelect", () => {
+ it("should render a combobox", () => {
+ mockUseInfiniteUsers.mockReturnValue(DEFAULT_HOOK_STATE);
+ render();
+ expect(screen.getByRole("combobox")).toBeInTheDocument();
+ });
+
+ it("should display user options when opened", async () => {
+ mockUseInfiniteUsers.mockReturnValue(DEFAULT_HOOK_STATE);
+ const user = userEvent.setup();
+ render();
+
+ await act(async () => {
+ await user.click(screen.getByRole("combobox"));
+ });
+
+ expect(await screen.findByText("alice@example.com")).toBeInTheDocument();
+ expect(screen.getByText("user-1")).toBeInTheDocument();
+ });
+
+ it("should call onChange with user_id when a user is selected", async () => {
+ mockUseInfiniteUsers.mockReturnValue(DEFAULT_HOOK_STATE);
+ const onChange = vi.fn();
+ const user = userEvent.setup();
+ render();
+
+ await act(async () => {
+ await user.click(screen.getByRole("combobox"));
+ });
+
+ await act(async () => {
+ await user.click(await screen.findByText("alice@example.com"));
+ });
+
+ expect(onChange).toHaveBeenCalledWith("user-1");
+ });
+
+ it("should call onChange with null when selection is cleared", async () => {
+ mockUseInfiniteUsers.mockReturnValue(DEFAULT_HOOK_STATE);
+ const onChange = vi.fn();
+ const user = userEvent.setup();
+ render();
+
+ const clearButton = document.querySelector(".ant-select-clear");
+ if (clearButton) {
+ await act(async () => {
+ await user.click(clearButton as Element);
+ });
+ expect(onChange).toHaveBeenCalledWith(null);
+ }
+ });
+
+ it("should pass search input to useInfiniteUsers as debounced value", async () => {
+ mockUseInfiniteUsers.mockReturnValue({ ...DEFAULT_HOOK_STATE, data: { pages: [] } });
+ const user = userEvent.setup();
+ render();
+
+ await act(async () => {
+ await user.click(screen.getByRole("combobox"));
+ await user.type(screen.getByRole("combobox"), "alice");
+ });
+
+ await waitFor(() => {
+ expect(mockUseInfiniteUsers).toHaveBeenCalledWith(
+ expect.any(Number),
+ "alice",
+ );
+ });
+ });
+
+ it("should show loading indicator when isLoading is true", () => {
+ mockUseInfiniteUsers.mockReturnValue({ ...DEFAULT_HOOK_STATE, isLoading: true });
+ render();
+ expect(document.querySelector(".ant-select-loading")).toBeInTheDocument();
+ });
+
+ it("should show user alias in label when alias is set", async () => {
+ mockUseInfiniteUsers.mockReturnValue(DEFAULT_HOOK_STATE);
+ const user = userEvent.setup();
+ render();
+
+ await act(async () => {
+ await user.click(screen.getByRole("combobox"));
+ });
+
+ expect(await screen.findByText("charlie@example.com")).toBeInTheDocument();
+ });
+
+ it("should use custom placeholder when provided", () => {
+ mockUseInfiniteUsers.mockReturnValue(DEFAULT_HOOK_STATE);
+ render();
+ expect(screen.getByText("Find a user...")).toBeInTheDocument();
+ });
+
+ it("should add ant-select-disabled class when disabled prop is true", () => {
+ mockUseInfiniteUsers.mockReturnValue(DEFAULT_HOOK_STATE);
+ const { container } = render();
+ expect(container.querySelector(".ant-select-disabled")).toBeTruthy();
+ });
+
+ it("should show fetchNextPage spinner when isFetchingNextPage is true", async () => {
+ mockUseInfiniteUsers.mockReturnValue({
+ ...DEFAULT_HOOK_STATE,
+ hasNextPage: true,
+ isFetchingNextPage: true,
+ });
+ const user = userEvent.setup();
+ render();
+
+ await act(async () => {
+ await user.click(screen.getByRole("combobox"));
+ });
+
+ const spinners = document.querySelectorAll(".anticon-loading");
+ expect(spinners.length).toBeGreaterThanOrEqual(1);
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/common_components/user_single_select.tsx b/ui/litellm-dashboard/src/components/common_components/user_single_select.tsx
new file mode 100644
index 000000000000..8c9874a1d86d
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/common_components/user_single_select.tsx
@@ -0,0 +1,123 @@
+import React, { useMemo, useState, type UIEvent } from "react";
+import { Select, Typography } from "antd";
+import { LoadingOutlined } from "@ant-design/icons";
+import { useDebouncedState } from "@tanstack/react-pacer/debouncer";
+import { useInfiniteUsers } from "@/app/(dashboard)/hooks/users/useUsers";
+
+const { Text } = Typography;
+
+interface UserSingleSelectProps {
+ value?: string | null;
+ onChange?: (value: string | null) => void;
+ disabled?: boolean;
+ pageSize?: number;
+ placeholder?: string;
+}
+
+const SCROLL_THRESHOLD = 0.8;
+const DEBOUNCE_MS = 300;
+
+/**
+ * A single-select dropdown for users with server-side debounced search and
+ * infinite scroll. Mirrors TeamMultiSelect but uses single-select mode to
+ * match the `/user/daily/activity` API contract that accepts one user_id.
+ */
+const UserSingleSelect: React.FC = ({
+ value,
+ onChange,
+ disabled,
+ pageSize = 50,
+ placeholder = "Search users by email or ID...",
+}) => {
+ const [searchInput, setSearchInput] = useState("");
+ const [debouncedSearch, setDebouncedSearch] = useDebouncedState("", {
+ wait: DEBOUNCE_MS,
+ });
+
+ const {
+ data,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ isLoading,
+ } = useInfiniteUsers(pageSize, debouncedSearch || undefined);
+
+ const userOptions = useMemo(() => {
+ if (!data?.pages) return [];
+ const seen = new Set();
+ const result: { value: string; label: string; email: string | null }[] = [];
+ for (const page of data.pages) {
+ for (const user of page.users) {
+ if (seen.has(user.user_id)) continue;
+ seen.add(user.user_id);
+ result.push({
+ value: user.user_id,
+ label: user.user_alias
+ ? `${user.user_alias} (${user.user_id})`
+ : user.user_email
+ ? `${user.user_email} (${user.user_id})`
+ : user.user_id,
+ email: user.user_email ?? null,
+ });
+ }
+ }
+ return result;
+ }, [data]);
+
+ const handlePopupScroll = (e: UIEvent) => {
+ const target = e.currentTarget;
+ const scrollRatio =
+ (target.scrollTop + target.clientHeight) / target.scrollHeight;
+ if (scrollRatio >= SCROLL_THRESHOLD && hasNextPage && !isFetchingNextPage) {
+ fetchNextPage();
+ }
+ };
+
+ const handleSearch = (val: string) => {
+ setSearchInput(val);
+ setDebouncedSearch(val);
+ };
+
+ return (
+
+ );
+};
+
+export default UserSingleSelect;