From 91cdc0114980d6ce477befb7c48eb0d42eaabe61 Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Thu, 27 Feb 2025 18:11:03 -0800 Subject: [PATCH] Allow team/org filters to be searchable on the Create Key Page (#8881) * fix(filtercomponent): always show apply filters button fix hiding behavior * style(create_key_button.tsx): style improvements on create key modal remove the numbering * feat(filter.tsx): allow searching team by team alias * style(filter.tsx): style improvements to ensure dropdown + custom value works as expected * style(filter.tsx): add explicit button allowing reset filters * fix(filter.tsx): fix linting error * feat(all_keys_table.tsx): show team alias on keys table * style(all_keys_table.tsx): enforce length constraints on table make it easier to see all columns --- litellm/proxy/_new_secret_config.yaml | 29 ++-- .../src/components/all_keys_table.tsx | 37 ++++- .../components/common_components/filter.tsx | 151 ++++++++++++++---- .../src/components/create_key_button.tsx | 8 +- .../organization_search_fn.tsx | 26 +++ .../key_team_helpers/team_search_fn.tsx | 22 +++ 6 files changed, 218 insertions(+), 55 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/key_team_helpers/organization_search_fn.tsx create mode 100644 ui/litellm-dashboard/src/components/key_team_helpers/team_search_fn.tsx diff --git a/litellm/proxy/_new_secret_config.yaml b/litellm/proxy/_new_secret_config.yaml index 400c6e057b..d9180abd4e 100644 --- a/litellm/proxy/_new_secret_config.yaml +++ b/litellm/proxy/_new_secret_config.yaml @@ -1,15 +1,22 @@ model_list: - - model_name: gpt-4o-mini + - model_name: claude-3.7 litellm_params: - custom_llm_provider: azure - model: gpt-4o-mini - api_version: "2024-10-21" - api_base: os.environ/AZURE_API_BASE - api_key: os.environ/AZURE_API_KEY + model: openai/gpt-3.5-turbo + api_key: os.environ/OPENAI_API_KEY + api_base: http://0.0.0.0:8090 + - model_name: deepseek-r1 + litellm_params: + model: bedrock/deepseek_r1/arn:aws:bedrock:us-west-2:888602223428:imported-model/bnnr6463ejgf + - model_name: deepseek-r1-api + litellm_params: + model: deepseek/deepseek-reasoner + - model_name: cohere.embed-english-v3 + litellm_params: + model: bedrock/cohere.embed-english-v3 + api_key: os.environ/COHERE_API_KEY + - model_name: bedrock-claude-3-7 + litellm_params: + model: bedrock/us.anthropic.claude-3-7-sonnet-20250219-v1:0 litellm_settings: - callbacks: ["prometheus"] - -general_settings: - disable_spend_logs: true - \ No newline at end of file + callbacks: ["langfuse"] \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/all_keys_table.tsx b/ui/litellm-dashboard/src/components/all_keys_table.tsx index 66644da6d3..b92294223a 100644 --- a/ui/litellm-dashboard/src/components/all_keys_table.tsx +++ b/ui/litellm-dashboard/src/components/all_keys_table.tsx @@ -8,7 +8,10 @@ import KeyInfoView from "./key_info_view"; import { Tooltip } from "antd"; import { Team, KeyResponse } from "./key_team_helpers/key_list"; import FilterComponent from "./common_components/filter"; +import { FilterOption } from "./common_components/filter"; import { Organization } from "./networking"; +import { createTeamSearchFunction } from "./key_team_helpers/team_search_fn"; +import { createOrgSearchFunction } from "./key_team_helpers/organization_search_fn"; interface AllKeysTableProps { keys: KeyResponse[]; isLoading?: boolean; @@ -171,21 +174,43 @@ export function AllKeysTable({ accessorKey: "key_name", cell: (info) => {info.getValue() as string}, }, + { + header: "Team Alias", + 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); + return team?.team_alias || "Unknown"; + }, + }, { header: "Team ID", accessorKey: "team_id", - cell: (info) => info.getValue() ? info.renderValue() : "Not Set", + cell: (info) => {info.getValue() ? `${(info.getValue() as string).slice(0, 7)}...` : "Not Set"} }, + { header: "Key Alias", accessorKey: "key_alias", - cell: (info) => info.getValue() ? info.renderValue() : "Not Set", + cell: (info) => {info.getValue() ? `${(info.getValue() as string).slice(0, 7)}...` : "Not Set"} }, { header: "Organization ID", accessorKey: "organization_id", - cell: (info) => info.getValue() ? info.renderValue() : "Not Set", + cell: (info) => {info.getValue() ? `${(info.getValue() as string).slice(0, 7)}...` : "Not Set"} }, + // { + // header: "User Email", + // accessorKey: "user_id", + // cell: (info) => { + // const userId = info.getValue() as string; + // return userId ? ( + // + // {userId.slice(0, 5)}... + // + // ) : "Not Set"; + // }, + // }, { header: "User ID", accessorKey: "user_id", @@ -272,9 +297,9 @@ export function AllKeysTable({ }, ]; - const filterOptions = [ - { name: 'Team ID', label: 'Team ID' }, - { name: 'Organization ID', label: 'Organization ID' } + 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) } ]; diff --git a/ui/litellm-dashboard/src/components/common_components/filter.tsx b/ui/litellm-dashboard/src/components/common_components/filter.tsx index 3dcd96521e..7c3f3c2d52 100644 --- a/ui/litellm-dashboard/src/components/common_components/filter.tsx +++ b/ui/litellm-dashboard/src/components/common_components/filter.tsx @@ -1,17 +1,21 @@ -import React, { useState, useRef, useEffect } from 'react'; -import { Button, Input, Dropdown, MenuProps } from 'antd'; +import React, { useState, useRef, useEffect, useCallback } from 'react'; +import { Button, Input, Dropdown, MenuProps, Select, Spin } from 'antd'; import { Card, Button as TremorButton } from '@tremor/react'; import { FilterIcon, XIcon, CheckIcon, ChevronDownIcon, - ChevronUpIcon + ChevronUpIcon, + SearchIcon } from '@heroicons/react/outline'; +import debounce from 'lodash/debounce'; -interface FilterOption { +export interface FilterOption { name: string; label?: string; + isSearchable?: boolean; + searchFn?: (searchText: string) => Promise>; } interface FilterValues { @@ -38,6 +42,9 @@ const FilterComponent: React.FC = ({ const [filterValues, setFilterValues] = useState(initialValues); const [tempValues, setTempValues] = useState(initialValues); const [dropdownOpen, setDropdownOpen] = useState(false); + const [searchOptions, setSearchOptions] = useState>([]); + const [searchLoading, setSearchLoading] = useState(false); + const [searchInputValue, setSearchInputValue] = useState(''); const filtersRef = useRef(null); @@ -46,22 +53,40 @@ const FilterComponent: React.FC = ({ const target = event.target as HTMLElement; if (filtersRef.current && !filtersRef.current.contains(target) && - !target.closest('.ant-dropdown')) { + !target.closest('.ant-dropdown') && + !target.closest('.ant-select-dropdown')) { setShowFilters(false); } }; - document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); + const debouncedSearch = useCallback( + debounce(async (value: string, option: FilterOption) => { + if (!value || !option.isSearchable || !option.searchFn) return; + + setSearchLoading(true); + try { + const results = await option.searchFn(value); + setSearchOptions(results); + } catch (error) { + console.error('Error searching:', error); + setSearchOptions([]); + } finally { + setSearchLoading(false); + } + }, 300), + [] + ); + const handleFilterChange = (value: string) => { setTempValues(prev => ({ ...prev, [selectedFilter]: value })); }; - + const clearFilters = () => { const emptyValues: FilterValues = {}; options.forEach(option => { @@ -69,13 +94,13 @@ const FilterComponent: React.FC = ({ }); setTempValues(emptyValues); }; - + const handleApplyFilters = () => { setFilterValues(tempValues); onApplyFilters(tempValues); setShowFilters(false); }; - + const dropdownItems: MenuProps['items'] = options.map(option => ({ key: option.name, label: ( @@ -88,6 +113,20 @@ const FilterComponent: React.FC = ({ ), })); + const currentOption = options.find(option => option.name === selectedFilter); + + const resetFilters = () => { + const emptyValues: FilterValues = {}; + options.forEach(option => { + emptyValues[option.name] = ''; + }); + setTempValues(emptyValues); + setFilterValues(emptyValues); + setSearchInputValue(''); + setSearchOptions([]); + onResetFilters(); // Call the parent's reset function + }; + return (
= ({ > {buttonLabel} - {showFilters && (
@@ -112,6 +150,7 @@ const FilterComponent: React.FC = ({ onClick: ({ key }) => { setSelectedFilter(key); setDropdownOpen(false); + setSearchOptions([]); } }} onOpenChange={setDropdownOpen} @@ -119,7 +158,7 @@ const FilterComponent: React.FC = ({ trigger={['click']} > - handleFilterChange(e.target.value)} - className="px-3 py-1.5 border rounded-md text-sm flex-1 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - suffix={ - tempValues[selectedFilter] ? ( - { - e.stopPropagation(); - handleFilterChange(''); - }} - /> - ) : null + {currentOption?.isSearchable ? ( + handleFilterChange(e.target.value)} + className="px-3 py-1.5 border rounded-md text-sm flex-1 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + suffix={ + tempValues[selectedFilter] ? ( + { + e.stopPropagation(); + handleFilterChange(''); + }} + /> + ) : null + } + /> + )}
- -
+ + +
-
diff --git a/ui/litellm-dashboard/src/components/create_key_button.tsx b/ui/litellm-dashboard/src/components/create_key_button.tsx index a468c25827..0ec06348e8 100644 --- a/ui/litellm-dashboard/src/components/create_key_button.tsx +++ b/ui/litellm-dashboard/src/components/create_key_button.tsx @@ -339,7 +339,7 @@ const CreateKey: React.FC = ({ + Create New Key = ({ > {/* Section 1: Key Ownership */}
- 1. Key Ownership + Key Ownership @@ -444,7 +444,7 @@ const CreateKey: React.FC = ({ {/* Section 2: Key Details */}
- 2. Key Details + Key Details @@ -503,7 +503,7 @@ const CreateKey: React.FC = ({
- 3. Optional Settings + Optional Settings { + return async (searchText: string): Promise> => { + if (!organizations || !searchText.trim()) { + return []; + } + + // Find organizations that match the search text by alias + const matchingOrgs: Array<{ label: string; value: string }> = []; + + organizations.forEach(org => { + if ( + org.organization_alias && + org.organization_alias.toLowerCase().includes(searchText.toLowerCase()) + ) { + matchingOrgs.push({ + label: `${org.organization_alias} (${org.organization_id})`, + value: org.organization_id || "" + }); + } + }); + + return matchingOrgs; + }; + }; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/team_search_fn.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/team_search_fn.tsx new file mode 100644 index 0000000000..b9522546ed --- /dev/null +++ b/ui/litellm-dashboard/src/components/key_team_helpers/team_search_fn.tsx @@ -0,0 +1,22 @@ +import { Team } from "./key_list"; + +export const createTeamSearchFunction = (teams: Team[] | null) => { + return async (searchText: string): Promise> => { + // Return empty array if teams is null or searchText is empty + if (!teams || !searchText.trim()) { + return []; + } + + // Filter teams where team_alias contains the search text (case insensitive) + const filteredTeams = teams.filter(team => + team.team_alias.toLowerCase().includes(searchText.toLowerCase()) + ); + + // Map filtered teams to the required format + return filteredTeams.map(team => ({ + label: `${team.team_alias} (${team.team_id.substring(0, 8)}...)`, + value: team.team_id + })); + }; + }; + \ No newline at end of file