From e38b0cab8b042ea93a1d7dfa13d141cc40d5fda4 Mon Sep 17 00:00:00 2001 From: Christian Owusu <36159205+crisshaker@users.noreply.github.com> Date: Thu, 24 Apr 2025 07:56:43 +0000 Subject: [PATCH 1/2] Virtual Keys: Filter by User ID --- .../src/components/all_keys_table.tsx | 26 +++++++++++-- .../key_team_helpers/filter_helpers.ts | 38 ++++++++++++++++++- .../key_team_helpers/filter_logic.tsx | 29 ++++++++++++-- .../components/key_team_helpers/key_list.tsx | 9 ++++- .../src/components/networking.tsx | 11 ++++-- .../src/components/user_dashboard.tsx | 3 ++ .../src/components/view_key_table.tsx | 9 ++++- 7 files changed, 111 insertions(+), 14 deletions(-) diff --git a/ui/litellm-dashboard/src/components/all_keys_table.tsx b/ui/litellm-dashboard/src/components/all_keys_table.tsx index 403b131c65..70bdcf4f6f 100644 --- a/ui/litellm-dashboard/src/components/all_keys_table.tsx +++ b/ui/litellm-dashboard/src/components/all_keys_table.tsx @@ -32,6 +32,8 @@ interface AllKeysTableProps { setSelectedTeam: (team: Team | null) => void; selectedKeyAlias: string | null; setSelectedKeyAlias: Setter; + selectedUserId: string | null; + setSelectedUserId: Setter; accessToken: string | null; userID: string | null; userRole: string | null; @@ -98,10 +100,9 @@ export function AllKeysTable({ onPageChange, pageSize = 50, teams, - selectedTeam, setSelectedTeam, - selectedKeyAlias, setSelectedKeyAlias, + setSelectedUserId, accessToken, userID, userRole, @@ -117,6 +118,7 @@ export function AllKeysTable({ filters, filteredKeys, allKeyAliases, + allUsers, allTeams, allOrganizations, handleFilterChange, @@ -128,7 +130,8 @@ export function AllKeysTable({ accessToken, setSelectedTeam, setCurrentOrg, - setSelectedKeyAlias + setSelectedKeyAlias, + setSelectedUserId }); useEffect(() => { @@ -363,6 +366,23 @@ export function AllKeysTable({ })); } }, + { + name: "User ID", + label: "User ID", + isSearchable: true, + searchFn: async (searchText) => { + const filteredUserIds = allUsers.filter(user => { + return user.user_id.toLowerCase().includes(searchText.toLowerCase()) + }); + + return filteredUserIds.map((user) => { + return { + label: user.user_id, + value: user.user_id + } + }); + } + }, { name: "Key Alias", label: "Key Alias", diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts b/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts index 7e99c7f9fc..28b81f5834 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts +++ b/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts @@ -1,6 +1,7 @@ -import { keyListCall, teamListCall, organizationListCall } from '../networking'; +import { keyListCall, teamListCall, organizationListCall, userListCall } from '../networking'; import { Team } from './key_list'; import { Organization } from '../networking'; +import { UserInfo } from '../view_users/types'; /** * Fetches all key aliases across all pages @@ -22,6 +23,7 @@ export const fetchAllKeyAliases = async (accessToken: string | null): Promise => { + if (!accessToken) return []; + + try { + let allUsers: any[] = []; + let currentPage = 1; + let hasMorePages = true; + + while (hasMorePages) { + const response = await userListCall( + accessToken, + null, // userIDs + currentPage, + 100, // page size + ); + + // Add users from the response + allUsers = [...allUsers, ...response.users]; + + // Check if there are more pages + if (currentPage < response.total_pages) { + currentPage++; + } else { + hasMorePages = false; + } + } + + return allUsers; + } catch (error) { + console.error("Error fetching all users:", error); + return []; + } +}; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/filter_logic.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/filter_logic.tsx index cc3d0eea38..b13cf10aa6 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/filter_logic.tsx +++ b/ui/litellm-dashboard/src/components/key_team_helpers/filter_logic.tsx @@ -3,13 +3,14 @@ import { KeyResponse } from "../key_team_helpers/key_list"; import { Organization } from "../networking"; import { Team } from "../key_team_helpers/key_list"; import { useQuery } from "@tanstack/react-query"; -import { fetchAllKeyAliases, fetchAllOrganizations, fetchAllTeams } from "./filter_helpers"; +import { fetchAllKeyAliases, fetchAllOrganizations, fetchAllTeams, fetchAllUsers } from "./filter_helpers"; import { Setter } from "@/types"; export interface FilterState { 'Team ID': string; 'Organization ID': string; + 'User ID': string; 'Key Alias': string; [key: string]: string; } @@ -21,7 +22,8 @@ export function useFilterLogic({ accessToken, setSelectedTeam, setCurrentOrg, - setSelectedKeyAlias + setSelectedKeyAlias, + setSelectedUserId }: { keys: KeyResponse[]; teams: Team[] | null; @@ -30,10 +32,12 @@ export function useFilterLogic({ setSelectedTeam: (team: Team | null) => void; setCurrentOrg: React.Dispatch>; setSelectedKeyAlias: Setter + setSelectedUserId: Setter }) { const [filters, setFilters] = useState({ 'Team ID': '', 'Organization ID': '', + 'User ID': '', 'Key Alias': '' }); const [allTeams, setAllTeams] = useState(teams || []); @@ -86,7 +90,7 @@ export function useFilterLogic({ } }, [accessToken]); - const queryAllKeysQuery = useQuery({ + const allKeyAliasesQuery = useQuery({ queryKey: ['allKeys'], queryFn: async () => { if (!accessToken) throw new Error('Access token required'); @@ -94,7 +98,18 @@ export function useFilterLogic({ }, enabled: !!accessToken }); - const allKeyAliases = queryAllKeysQuery.data || [] + const allKeyAliases = allKeyAliasesQuery.data || [] + + const allUsersQuery = useQuery({ + queryKey: ['allUsers'], + enabled: !!accessToken, + queryFn: async () => { + if (!accessToken) throw new Error('Access token required'); + return await fetchAllUsers(accessToken); + }, + }); + const allUsers = allUsersQuery.data || []; + // Update teams and organizations when props change useEffect(() => { @@ -120,6 +135,7 @@ export function useFilterLogic({ setFilters({ 'Team ID': newFilters['Team ID'] || '', 'Organization ID': newFilters['Organization ID'] || '', + 'User ID': newFilters['User ID'] || '', 'Key Alias': newFilters['Key Alias'] || '' }); @@ -144,6 +160,8 @@ export function useFilterLogic({ ? allKeyAliases.find((k) => k === keyAlias) || null : null; setSelectedKeyAlias(selectedKeyAlias) + + setSelectedUserId(newFilters['User ID'] || null) }; const handleFilterReset = () => { @@ -151,6 +169,7 @@ export function useFilterLogic({ setFilters({ 'Team ID': '', 'Organization ID': '', + 'User ID': '', 'Key Alias': '' }); @@ -158,6 +177,7 @@ export function useFilterLogic({ setSelectedTeam(null); setCurrentOrg(null); setSelectedKeyAlias(null); + setSelectedUserId(null) }; return { @@ -165,6 +185,7 @@ export function useFilterLogic({ filteredKeys, allKeyAliases, allTeams, + allUsers, allOrganizations, handleFilterChange, handleFilterReset diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx index 98d8e0e499..5fe8891751 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx +++ b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx @@ -86,6 +86,7 @@ interface UseKeyListProps { selectedTeam?: Team; currentOrg: Organization | null; selectedKeyAlias: string | null; +selectedUserId: string | null; accessToken: string; currentPage?: number; } @@ -110,6 +111,7 @@ const useKeyList = ({ selectedTeam, currentOrg, selectedKeyAlias, + selectedUserId, accessToken, currentPage = 1, }: UseKeyListProps): UseKeyListReturn => { @@ -136,6 +138,7 @@ const useKeyList = ({ currentOrg?.organization_id || null, selectedTeam?.team_id || "", selectedKeyAlias, + selectedUserId, params.page as number || 1, 50, ); @@ -159,9 +162,11 @@ const useKeyList = ({ 'accessToken', accessToken, 'selectedKeyAlias', - selectedKeyAlias + selectedKeyAlias, + 'selectedUserId', + selectedUserId ); - }, [selectedTeam, currentOrg, accessToken, selectedKeyAlias]); + }, [selectedTeam, currentOrg, accessToken, selectedKeyAlias, selectedUserId]); const setKeys = (newKeysOrUpdater: KeyResponse[] | ((prevKeys: KeyResponse[]) => KeyResponse[])) => { setKeyData(prevData => { diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 63a4d6f101..42bc35e540 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -2605,7 +2605,8 @@ export const keyListCall = async ( accessToken: String, organizationID: string | null, teamID: string | null, - selectedKeyAlias: string | null, + keyAlias: string | null, + userId: string | null, page: number, pageSize: number, ) => { @@ -2625,8 +2626,12 @@ export const keyListCall = async ( queryParams.append('organization_id', organizationID.toString()); } - if (selectedKeyAlias) { - queryParams.append('key_alias', selectedKeyAlias) + if (keyAlias) { + queryParams.append('key_alias', keyAlias) + } + + if (userId) { + queryParams.append('user_id', userId) } if (page) { diff --git a/ui/litellm-dashboard/src/components/user_dashboard.tsx b/ui/litellm-dashboard/src/components/user_dashboard.tsx index 4279c124d0..4ebd020902 100644 --- a/ui/litellm-dashboard/src/components/user_dashboard.tsx +++ b/ui/litellm-dashboard/src/components/user_dashboard.tsx @@ -110,6 +110,7 @@ const UserDashboard: React.FC = ({ }; const [selectedTeam, setSelectedTeam] = useState(null); const [selectedKeyAlias, setSelectedKeyAlias] = useState(null); + const [selectedUserId, setSelectedUserId] = useState(null); // check if window is not undefined if (typeof window !== "undefined") { window.addEventListener("beforeunload", function () { @@ -428,6 +429,8 @@ const UserDashboard: React.FC = ({ setSelectedTeam={setSelectedTeam} selectedKeyAlias={selectedKeyAlias} setSelectedKeyAlias={setSelectedKeyAlias} + selectedUserId={selectedUserId} + setSelectedUserId={setSelectedUserId} data={keys} setData={setKeys} premiumUser={premiumUser} diff --git a/ui/litellm-dashboard/src/components/view_key_table.tsx b/ui/litellm-dashboard/src/components/view_key_table.tsx index 715bf6b7f7..8e1d13dbd8 100644 --- a/ui/litellm-dashboard/src/components/view_key_table.tsx +++ b/ui/litellm-dashboard/src/components/view_key_table.tsx @@ -110,6 +110,8 @@ interface ViewKeyTableProps { setCurrentOrg: React.Dispatch>; selectedKeyAlias: string | null; setSelectedKeyAlias: Setter; + selectedUserId: string | null; + setSelectedUserId: Setter; } interface ItemData { @@ -159,7 +161,9 @@ const ViewKeyTable: React.FC = ({ organizations, setCurrentOrg, selectedKeyAlias, - setSelectedKeyAlias + setSelectedKeyAlias, + selectedUserId, + setSelectedUserId }) => { const [isButtonClicked, setIsButtonClicked] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -185,6 +189,7 @@ const ViewKeyTable: React.FC = ({ selectedTeam, currentOrg, selectedKeyAlias, + selectedUserId, accessToken, }); @@ -440,6 +445,8 @@ const ViewKeyTable: React.FC = ({ refresh={refresh} selectedKeyAlias={selectedKeyAlias} setSelectedKeyAlias={setSelectedKeyAlias} + selectedUserId={selectedUserId} + setSelectedUserId={setSelectedUserId} /> {isDeleteModalOpen && ( From 1af8d64e77bc21948b105b3d50fcf22b96d4dc77 Mon Sep 17 00:00:00 2001 From: Christian Owusu <36159205+crisshaker@users.noreply.github.com> Date: Thu, 24 Apr 2025 07:58:01 +0000 Subject: [PATCH 2/2] Cleanup --- ui/litellm-dashboard/src/components/all_keys_table.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/litellm-dashboard/src/components/all_keys_table.tsx b/ui/litellm-dashboard/src/components/all_keys_table.tsx index 70bdcf4f6f..18259c96c9 100644 --- a/ui/litellm-dashboard/src/components/all_keys_table.tsx +++ b/ui/litellm-dashboard/src/components/all_keys_table.tsx @@ -371,11 +371,11 @@ export function AllKeysTable({ label: "User ID", isSearchable: true, searchFn: async (searchText) => { - const filteredUserIds = allUsers.filter(user => { + const filteredUsers = allUsers.filter(user => { return user.user_id.toLowerCase().includes(searchText.toLowerCase()) }); - return filteredUserIds.map((user) => { + return filteredUsers.map((user) => { return { label: user.user_id, value: user.user_id