From a7f6a95dd9bd99b041b935567613e4c09ca68fcf Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Sat, 8 Mar 2025 15:58:29 -0800 Subject: [PATCH] (UI) - Fix, Allow Filter Keys by Team Alias, Key Alias and Org (#9083) * working team, key, org filters * fix size of filter * all keys filter * 1 file for filter logic * remove key alias check --- .../src/components/all_keys_table.tsx | 110 +++++++------ .../components/common_components/filter.tsx | 54 +++++-- .../key_team_helpers/filter_helpers.ts | 127 +++++++++++++++ .../key_team_helpers/filter_logic.tsx | 148 ++++++++++++++++++ 4 files changed, 380 insertions(+), 59 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.ts create mode 100644 ui/litellm-dashboard/src/components/key_team_helpers/filter_logic.tsx diff --git a/ui/litellm-dashboard/src/components/all_keys_table.tsx b/ui/litellm-dashboard/src/components/all_keys_table.tsx index 1f05c243ee2..bc0d1135ea7 100644 --- a/ui/litellm-dashboard/src/components/all_keys_table.tsx +++ b/ui/litellm-dashboard/src/components/all_keys_table.tsx @@ -12,6 +12,8 @@ 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 { useFilterLogic } from "./key_team_helpers/filter_logic"; + interface AllKeysTableProps { keys: KeyResponse[]; isLoading?: boolean; @@ -95,17 +97,28 @@ export function AllKeysTable({ userID, userRole, organizations, - setCurrentOrg + setCurrentOrg, }: AllKeysTableProps) { const [selectedKeyId, setSelectedKeyId] = useState(null); - const [filters, setFilters] = useState<{ - 'Team ID': string; - 'Organization ID': string; - }>({ - 'Team ID': '', - 'Organization ID': '' - }); const [userList, setUserList] = useState([]); + + // Use the filter logic hook + const { + filters, + filteredKeys, + allKeyAliases, + allTeams, + allOrganizations, + handleFilterChange, + handleFilterReset + } = useFilterLogic({ + keys, + teams, + organizations, + accessToken, + setSelectedTeam, + setCurrentOrg + }); useEffect(() => { if (accessToken) { @@ -118,43 +131,6 @@ export function AllKeysTable({ } }, [accessToken, keys]); - const handleFilterChange = (newFilters: Record) => { - // Update filters state - setFilters({ - 'Team ID': newFilters['Team ID'] || '', - 'Organization ID': newFilters['Organization ID'] || '' - }); - - // Handle Team change - if (newFilters['Team ID']) { - const selectedTeamData = teams?.find(team => team.team_id === newFilters['Team ID']); - if (selectedTeamData) { - setSelectedTeam(selectedTeamData); - } - } - - // Handle Org change - if (newFilters['Organization ID']) { - const selectedOrg = organizations?.find(org => org.organization_id === newFilters['Organization ID']); - if (selectedOrg) { - setCurrentOrg(selectedOrg); - } - } - }; - - const handleFilterReset = () => { - // Reset filters state - setFilters({ - 'Team ID': '', - 'Organization ID': '' - }); - - // Reset team and org selections - setSelectedTeam(null); // or whatever your default value should be - setCurrentOrg(null); // or whatever your default value should be - }; - - const columns: ColumnDef[] = [ { id: "expander", @@ -205,7 +181,7 @@ export function AllKeysTable({ accessorKey: "team_id", // Change to access the team_id cell: ({ row, getValue }) => { const teamId = getValue() as string; - const team = teams?.find(t => t.team_id === teamId); + const team = allTeams?.find(t => t.team_id === teamId); return team?.team_alias || "Unknown"; }, }, @@ -323,8 +299,42 @@ 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 + })); + } + }, ]; @@ -338,7 +348,7 @@ export function AllKeysTable({ accessToken={accessToken} userID={userID} userRole={userRole} - teams={teams} + teams={allTeams} /> ) : (
@@ -376,7 +386,7 @@ export function AllKeysTable({ col.id !== 'expander')} - data={keys} + data={filteredKeys} isLoading={isLoading} getRowCanExpand={() => false} renderSubComponent={() => <>} diff --git a/ui/litellm-dashboard/src/components/common_components/filter.tsx b/ui/litellm-dashboard/src/components/common_components/filter.tsx index 7c3f3c2d524..cca4f70b001 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 { @@ -139,7 +178,7 @@ const FilterComponent: React.FC = ({ {buttonLabel} {showFilters && ( - +
Where @@ -147,17 +186,14 @@ const FilterComponent: React.FC = ({ { - setSelectedFilter(key); - setDropdownOpen(false); - setSearchOptions([]); - } + onClick: ({ key }) => handleFilterSelect(key), + style: { minWidth: '200px' } }} onOpenChange={setDropdownOpen} open={dropdownOpen} trigger={['click']} > -