From ee8402fcce007248bbc3638a381ffaa88d41c384 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Sat, 8 Mar 2025 13:38:32 -0800 Subject: [PATCH] fix filters --- .../src/components/all_keys_table.tsx | 121 +++++++++++++++-- .../components/common_components/filter.tsx | 51 +++++-- .../key_team_helpers/filter_helpers.ts | 127 ++++++++++++++++++ 3 files changed, 282 insertions(+), 17 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts diff --git a/ui/litellm-dashboard/src/components/all_keys_table.tsx b/ui/litellm-dashboard/src/components/all_keys_table.tsx index 1f05c243ee2..844ecbc9e84 100644 --- a/ui/litellm-dashboard/src/components/all_keys_table.tsx +++ b/ui/litellm-dashboard/src/components/all_keys_table.tsx @@ -12,6 +12,9 @@ import { FilterOption } from "./common_components/filter"; import { Organization, userListCall } from "./networking"; import { createTeamSearchFunction } from "./key_team_helpers/team_search_fn"; import { createOrgSearchFunction } from "./key_team_helpers/organization_search_fn"; +import { keyListCall } from "./networking"; +import { fetchAllKeyAliases, fetchAllTeams, fetchAllOrganizations } from './key_team_helpers/filter_helpers'; + interface AllKeysTableProps { keys: KeyResponse[]; isLoading?: boolean; @@ -95,17 +98,66 @@ export function AllKeysTable({ userID, userRole, organizations, - setCurrentOrg + setCurrentOrg, }: AllKeysTableProps) { const [selectedKeyId, setSelectedKeyId] = useState(null); const [filters, setFilters] = useState<{ 'Team ID': string; 'Organization ID': string; + 'Key Alias': string; }>({ 'Team ID': '', - 'Organization ID': '' + 'Organization ID': '', + 'Key Alias': '' }); const [userList, setUserList] = useState([]); + const [allKeyAliases, setAllKeyAliases] = useState([]); + const [allTeams, setAllTeams] = useState(teams || []); + const [allOrganizations, setAllOrganizations] = useState(organizations || []); + + // Fetch all data for filters when component mounts + useEffect(() => { + const loadAllFilterData = async () => { + // Load all key aliases + const aliases = await fetchAllKeyAliases(accessToken); + setAllKeyAliases(aliases); + + // Load all teams - no organization filter needed here + const teamsData = await fetchAllTeams(accessToken); + if (teamsData.length > 0) { + setAllTeams(teamsData); + } + + // Load all organizations + const orgsData = await fetchAllOrganizations(accessToken); + if (orgsData.length > 0) { + setAllOrganizations(orgsData); + } + }; + + if (accessToken) { + loadAllFilterData(); + } + }, [accessToken]); + + // Update teams and organizations when props change + useEffect(() => { + if (teams && teams.length > 0) { + setAllTeams(prevTeams => { + // Only update if we don't already have a larger set of teams + return prevTeams.length < teams.length ? teams : prevTeams; + }); + } + }, [teams]); + + useEffect(() => { + if (organizations && organizations.length > 0) { + setAllOrganizations(prevOrgs => { + // Only update if we don't already have a larger set of organizations + return prevOrgs.length < organizations.length ? organizations : prevOrgs; + }); + } + }, [organizations]); useEffect(() => { if (accessToken) { @@ -122,7 +174,8 @@ export function AllKeysTable({ // Update filters state setFilters({ 'Team ID': newFilters['Team ID'] || '', - 'Organization ID': newFilters['Organization ID'] || '' + 'Organization ID': newFilters['Organization ID'] || '', + 'Key Alias': newFilters['Key Alias'] || '' }); // Handle Team change @@ -146,12 +199,13 @@ export function AllKeysTable({ // Reset filters state setFilters({ 'Team ID': '', - 'Organization ID': '' + 'Organization ID': '', + 'Key Alias': '' }); // Reset team and org selections - setSelectedTeam(null); // or whatever your default value should be - setCurrentOrg(null); // or whatever your default value should be + setSelectedTeam(null); + setCurrentOrg(null); }; @@ -323,8 +377,59 @@ export function AllKeysTable({ ]; const filterOptions: FilterOption[] = [ - { name: 'Team ID', label: 'Team ID', isSearchable: true, searchFn: createTeamSearchFunction(teams) }, - { name: 'Organization ID', label: 'Organization ID', isSearchable: true, searchFn: createOrgSearchFunction(organizations) } + { + name: 'Team ID', + label: 'Team ID', + isSearchable: true, + searchFn: async (searchText: string) => { + if (!allTeams || allTeams.length === 0) return []; + + const filteredTeams = allTeams.filter(team => + team.team_id.toLowerCase().includes(searchText.toLowerCase()) || + (team.team_alias && team.team_alias.toLowerCase().includes(searchText.toLowerCase())) + ); + + return filteredTeams.map(team => ({ + label: `${team.team_alias || team.team_id} (${team.team_id})`, + value: team.team_id + })); + } + }, + { + name: 'Organization ID', + label: 'Organization ID', + isSearchable: true, + searchFn: async (searchText: string) => { + if (!allOrganizations || allOrganizations.length === 0) return []; + + const filteredOrgs = allOrganizations.filter(org => + org.organization_id.toLowerCase().includes(searchText.toLowerCase()) || + (org.organization_name && org.organization_name.toLowerCase().includes(searchText.toLowerCase())) + ); + + return filteredOrgs.map(org => ({ + label: `${org.organization_name || 'Unknown'} (${org.organization_id})`, + value: org.organization_id + })); + } + }, + { + name: 'Key Alias', + label: 'Key Alias', + isSearchable: true, + searchFn: async (searchText: string) => { + if (!allKeyAliases || allKeyAliases.length === 0) return []; + + const filteredAliases = allKeyAliases.filter(alias => + alias && alias.toLowerCase().includes(searchText.toLowerCase()) + ); + + return filteredAliases.map(alias => ({ + label: alias, + value: alias + })); + } + } ]; diff --git a/ui/litellm-dashboard/src/components/common_components/filter.tsx b/ui/litellm-dashboard/src/components/common_components/filter.tsx index 7c3f3c2d524..d68b7f98312 100644 --- a/ui/litellm-dashboard/src/components/common_components/filter.tsx +++ b/ui/litellm-dashboard/src/components/common_components/filter.tsx @@ -62,9 +62,48 @@ const FilterComponent: React.FC = ({ return () => document.removeEventListener('mousedown', handleClickOutside); }, []); + useEffect(() => { + if (options.length > 0 && options[0].isSearchable && options[0].searchFn) { + loadInitialOptions(options[0]); + } + }, []); + + const loadInitialOptions = async (option: FilterOption) => { + if (!option.isSearchable || !option.searchFn) return; + + setSearchLoading(true); + try { + const results = await option.searchFn(''); + setSearchOptions(results); + } catch (error) { + console.error('Error loading initial options:', error); + setSearchOptions([]); + } finally { + setSearchLoading(false); + } + }; + + useEffect(() => { + if (showFilters && currentOption?.isSearchable && currentOption?.searchFn) { + loadInitialOptions(currentOption); + } + }, [showFilters, selectedFilter]); + + const handleFilterSelect = (key: string) => { + setSelectedFilter(key); + setDropdownOpen(false); + + const newOption = options.find(opt => opt.name === key); + if (newOption?.isSearchable && newOption?.searchFn) { + loadInitialOptions(newOption); + } else { + setSearchOptions([]); + } + }; + const debouncedSearch = useCallback( debounce(async (value: string, option: FilterOption) => { - if (!value || !option.isSearchable || !option.searchFn) return; + if (!option.isSearchable || !option.searchFn) return; setSearchLoading(true); try { @@ -124,7 +163,7 @@ const FilterComponent: React.FC = ({ setFilterValues(emptyValues); setSearchInputValue(''); setSearchOptions([]); - onResetFilters(); // Call the parent's reset function + onResetFilters(); }; return ( @@ -147,11 +186,7 @@ const FilterComponent: React.FC = ({ { - setSelectedFilter(key); - setDropdownOpen(false); - setSearchOptions([]); - } + onClick: ({ key }) => handleFilterSelect(key) }} onOpenChange={setDropdownOpen} open={dropdownOpen} @@ -179,7 +214,6 @@ const FilterComponent: React.FC = ({ }} onInputKeyDown={(e) => { if (e.key === 'Enter' && searchInputValue) { - // Allow manual entry of the value on Enter handleFilterChange(searchInputValue); e.preventDefault(); } @@ -198,7 +232,6 @@ const FilterComponent: React.FC = ({ className="p-0 mt-1" onClick={() => { handleFilterChange(searchInputValue); - // Close the dropdown/select after selecting the value const selectElement = document.activeElement as HTMLElement; if (selectElement) { selectElement.blur(); 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 new file mode 100644 index 00000000000..94fc738a204 --- /dev/null +++ b/ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts @@ -0,0 +1,127 @@ +import { keyListCall, teamListCall, organizationListCall } from '../networking'; +import { Team } from './key_list'; +import { Organization } from '../networking'; + +/** + * Fetches all key aliases across all pages + * @param accessToken The access token for API authentication + * @returns Array of all unique key aliases + */ +export const fetchAllKeyAliases = async (accessToken: string | null): Promise => { + if (!accessToken) return []; + + try { + // Fetch all pages of keys to extract aliases + let allAliases: string[] = []; + let currentPage = 1; + let hasMorePages = true; + + while (hasMorePages) { + const response = await keyListCall( + accessToken, + null, // organization_id + "", // team_id + currentPage, + 100 // larger page size to reduce number of requests + ); + + // Extract aliases from this page + const pageAliases = response.keys + .map(key => key.key_alias) + .filter(Boolean) as string[]; + + allAliases = [...allAliases, ...pageAliases]; + + // Check if there are more pages + if (currentPage < response.total_pages) { + currentPage++; + } else { + hasMorePages = false; + } + } + + // Remove duplicates + return [...new Set(allAliases)]; + } catch (error) { + console.error("Error fetching all key aliases:", error); + return []; + } +}; + +/** + * Fetches all teams across all pages + * @param accessToken The access token for API authentication + * @param organizationId Optional organization ID to filter teams + * @returns Array of all teams + */ +export const fetchAllTeams = async (accessToken: string | null, organizationId?: string | null): Promise => { + if (!accessToken) return []; + + try { + let allTeams: Team[] = []; + let currentPage = 1; + let hasMorePages = true; + + while (hasMorePages) { + const response = await teamListCall( + accessToken, + organizationId || null, + currentPage, + 100 // larger page size to reduce number of requests + ); + + // Add teams from this page + allTeams = [...allTeams, ...response.teams]; + + // Check if there are more pages + if (currentPage < response.total_pages) { + currentPage++; + } else { + hasMorePages = false; + } + } + + return allTeams; + } catch (error) { + console.error("Error fetching all teams:", error); + return []; + } +}; + +/** + * Fetches all organizations across all pages + * @param accessToken The access token for API authentication + * @returns Array of all organizations + */ +export const fetchAllOrganizations = async (accessToken: string | null): Promise => { + if (!accessToken) return []; + + try { + let allOrganizations: Organization[] = []; + let currentPage = 1; + let hasMorePages = true; + + while (hasMorePages) { + const response = await organizationListCall( + accessToken, + currentPage, + 100 // larger page size to reduce number of requests + ); + + // Add organizations from this page + allOrganizations = [...allOrganizations, ...response.organizations]; + + // Check if there are more pages + if (currentPage < response.total_pages) { + currentPage++; + } else { + hasMorePages = false; + } + } + + return allOrganizations; + } catch (error) { + console.error("Error fetching all organizations:", error); + return []; + } +}; \ No newline at end of file