diff --git a/litellm/proxy/management_endpoints/internal_user_endpoints.py b/litellm/proxy/management_endpoints/internal_user_endpoints.py index 162fabd1d78..02452d241df 100644 --- a/litellm/proxy/management_endpoints/internal_user_endpoints.py +++ b/litellm/proxy/management_endpoints/internal_user_endpoints.py @@ -902,12 +902,6 @@ async def get_user_key_counts( return result -@router.get( - "/user/get_users", - tags=["Internal User management"], - dependencies=[Depends(user_api_key_auth)], - response_model=UserListResponse, -) @router.get( "/user/list", tags=["Internal User management"], @@ -921,15 +915,19 @@ async def get_users( user_ids: Optional[str] = fastapi.Query( default=None, description="Get list of users by user_ids" ), + user_email: Optional[str] = fastapi.Query( + default=None, description="Filter users by partial email match" + ), + team: Optional[str] = fastapi.Query( + default=None, description="Filter users by team id" + ), page: int = fastapi.Query(default=1, ge=1, description="Page number"), page_size: int = fastapi.Query( default=25, ge=1, le=100, description="Number of items per page" ), ): """ - Get a paginated list of users, optionally filtered by role. - - Used by the UI to populate the user lists. + Get a paginated list of users with filtering options. Parameters: role: Optional[str] @@ -940,17 +938,17 @@ async def get_users( - internal_user_viewer user_ids: Optional[str] Get list of users by user_ids. Comma separated list of user_ids. + user_email: Optional[str] + Filter users by partial email match + team: Optional[str] + Filter users by team id. Will match if user has this team in their teams array. page: int The page number to return page_size: int The number of items per page - Currently - admin-only endpoint. - - Example curl: - ``` - http://0.0.0.0:4000/user/list?user_ids=default_user_id,693c1a4a-1cc0-4c7c-afe8-b5d2c8d52e17 - ``` + Returns: + UserListResponse with filtered and paginated users """ from litellm.proxy.proxy_server import prisma_client @@ -963,25 +961,30 @@ async def get_users( # Calculate skip and take for pagination skip = (page - 1) * page_size - # Prepare the query conditions # Build where conditions based on provided parameters where_conditions: Dict[str, Any] = {} if role: - where_conditions["user_role"] = { - "contains": role, - "mode": "insensitive", # Case-insensitive search - } + where_conditions["user_role"] = role # Exact match instead of contains if user_ids and isinstance(user_ids, str): user_id_list = [uid.strip() for uid in user_ids.split(",") if uid.strip()] where_conditions["user_id"] = { - "in": user_id_list, # Now passing a list of strings as required by Prisma + "in": user_id_list, } - users: Optional[ - List[LiteLLM_UserTable] - ] = await prisma_client.db.litellm_usertable.find_many( + if user_email: + where_conditions["user_email"] = { + "contains": user_email, + "mode": "insensitive", # Case-insensitive search + } + + if team: + where_conditions["teams"] = { + "has": team # Array contains for string arrays in Prisma + } + + users = await prisma_client.db.litellm_usertable.find_many( where=where_conditions, skip=skip, take=page_size, @@ -989,9 +992,7 @@ async def get_users( ) # Get total count of user rows - total_count = await prisma_client.db.litellm_usertable.count( - where=where_conditions # type: ignore - ) + total_count = await prisma_client.db.litellm_usertable.count(where=where_conditions) # Get key count for each user if users is not None: @@ -1014,7 +1015,7 @@ async def get_users( LiteLLM_UserTableWithKeyCount( **user.model_dump(), key_count=user_key_counts.get(user.user_id, 0) ) - ) # Return full key object + ) else: user_list = [] diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index f16aaf30fa8..a1fdf7d0738 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -676,6 +676,9 @@ export const userListCall = async ( userIDs: string[] | null = null, page: number | null = null, page_size: number | null = null, + userEmail: string | null = null, + userRole: string | null = null, + team: string | null = null, ) => { /** * Get all available teams on proxy @@ -698,6 +701,18 @@ export const userListCall = async ( if (page_size) { queryParams.append('page_size', page_size.toString()); } + + if (userEmail) { + queryParams.append('user_email', userEmail); + } + + if (userRole) { + queryParams.append('role', userRole); + } + + if (team) { + queryParams.append('team', team); + } const queryString = queryParams.toString(); if (queryString) { diff --git a/ui/litellm-dashboard/src/components/view_users.tsx b/ui/litellm-dashboard/src/components/view_users.tsx index 81806b974f8..390f19a6000 100644 --- a/ui/litellm-dashboard/src/components/view_users.tsx +++ b/ui/litellm-dashboard/src/components/view_users.tsx @@ -23,6 +23,7 @@ import { DialogPanel, Icon, TextInput, + NumberInput, } from "@tremor/react"; import { message } from "antd"; @@ -32,7 +33,7 @@ import { userInfoCall, userUpdateUserCall, getPossibleUserRoles, - userFilterUICall, + userListCall, } from "./networking"; import { Badge, BadgeDelta, Button } from "@tremor/react"; import RequestAccess from "./request_model_access"; @@ -79,6 +80,16 @@ interface CreateuserProps { onUserCreated: () => Promise; } +interface FilterState { + email: string; + user_id: string; + user_role: string; + team: string; + model: string; + min_spend: number | null; + max_spend: number | null; +} + const isLocal = process.env.NODE_ENV === "development"; const proxyBaseUrl = isLocal ? "http://localhost:4000" : null; if (isLocal != true) { @@ -110,6 +121,16 @@ const ViewUserDashboard: React.FC = ({ const defaultPageSize = 25; const [searchTerm, setSearchTerm] = useState(""); const [activeTab, setActiveTab] = useState("users"); + const [filters, setFilters] = useState({ + email: "", + user_id: "", + user_role: "", + team: "", + model: "", + min_spend: null, + max_spend: null + }); + const [showFilters, setShowFilters] = useState(false); // check if window is not undefined if (typeof window !== "undefined") { @@ -124,35 +145,38 @@ const ViewUserDashboard: React.FC = ({ setIsDeleteModalOpen(true); }; + const handleFilterChange = (key: keyof FilterState, value: string | number | null) => { + const newFilters = { ...filters, [key]: value }; + setFilters(newFilters); + debouncedSearch(newFilters); + }; + // Create a debounced version of the search function const debouncedSearch = useCallback( - debounce(async (searchValue: string) => { + debounce(async (filters: FilterState) => { if (!accessToken || !token || !userRole || !userID) { return; } try { - const params = new URLSearchParams(); - if (searchValue) { - params.append('user_email', searchValue); - } - const filteredUsers = await userFilterUICall(accessToken, params); + // Make the API call using userListCall with all filter parameters + const data = await userListCall( + accessToken, + filters.user_id ? [filters.user_id] : null, + 1, // Reset to first page when searching + defaultPageSize, + filters.email || null, + filters.user_role || null, + filters.team || null + ); - // Update the user list with filtered results - if (filteredUsers) { - setUserData(filteredUsers); - setUserListResponse(prev => ({ - ...prev, - users: filteredUsers, - total: filteredUsers.length, - page: 1, - page_size: defaultPageSize, - total_pages: Math.ceil(filteredUsers.length / defaultPageSize) - })); + if (data) { + setUserData(data.users); + setUserListResponse(data); } } catch (error) { console.error("Error searching users:", error); } - }, 300), // 300ms delay + }, 300), [accessToken, token, userRole, userID] ); @@ -168,7 +192,7 @@ const ViewUserDashboard: React.FC = ({ if (value === "") { refreshUserData(); // Reset to original data when search is cleared } else { - debouncedSearch(value); + debouncedSearch(filters); } }; @@ -266,14 +290,15 @@ const ViewUserDashboard: React.FC = ({ setUserListResponse(parsedData); setUserData(parsedData.users || []); } else { - // Fetch from API if not in cache - const userDataResponse = await userInfoCall( + // Fetch from API using userListCall with current filters + const userDataResponse = await userListCall( accessToken, - null, - userRole, - true, + filters.user_id ? [filters.user_id] : null, currentPage, - defaultPageSize + defaultPageSize, + filters.email || null, + filters.user_role || null, + filters.team || null ); // Store in session storage @@ -304,7 +329,7 @@ const ViewUserDashboard: React.FC = ({ fetchData(); } - }, [accessToken, token, userRole, userID, currentPage]); + }, [accessToken, token, userRole, userID, currentPage, filters]); if (!userData) { return
Loading...
; @@ -347,16 +372,63 @@ const ViewUserDashboard: React.FC = ({
-
-
+
+ {/* Search and Filter Controls */} +
handleSearch(e.target.value)} + value={filters.email} + onChange={(e) => handleFilterChange('email', e.target.value)} className="w-full" />
+ +
+ + {/* Advanced Filters */} + {showFilters && ( +
+ handleFilterChange('user_id', e.target.value)} + /> + + + + {/* */} +
+ )} + + {/* Results Count */} +
Showing{" "} {userListResponse && userListResponse.users && userListResponse.users.length > 0 @@ -371,32 +443,10 @@ const ViewUserDashboard: React.FC = ({ : 0}{" "} of {userListResponse ? userListResponse.total : 0} results -
- - - Page {userListResponse ? userListResponse.page : "-"} of{" "} - {userListResponse ? userListResponse.total_pages : "-"} - - -
+