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
This commit is contained in:
Krish Dholakia
2025-02-27 18:11:03 -08:00
committed by GitHub
parent 1e7b9cf767
commit 91cdc01149
6 changed files with 218 additions and 55 deletions
+18 -11
View File
@@ -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
callbacks: ["langfuse"]
@@ -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) => <span className="font-mono text-xs">{info.getValue() as string}</span>,
},
{
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) => <Tooltip title={info.getValue() as string}>{info.getValue() ? `${(info.getValue() as string).slice(0, 7)}...` : "Not Set"}</Tooltip>
},
{
header: "Key Alias",
accessorKey: "key_alias",
cell: (info) => info.getValue() ? info.renderValue() : "Not Set",
cell: (info) => <Tooltip title={info.getValue() as string}>{info.getValue() ? `${(info.getValue() as string).slice(0, 7)}...` : "Not Set"}</Tooltip>
},
{
header: "Organization ID",
accessorKey: "organization_id",
cell: (info) => info.getValue() ? info.renderValue() : "Not Set",
cell: (info) => <Tooltip title={info.getValue() as string}>{info.getValue() ? `${(info.getValue() as string).slice(0, 7)}...` : "Not Set"}</Tooltip>
},
// {
// header: "User Email",
// accessorKey: "user_id",
// cell: (info) => {
// const userId = info.getValue() as string;
// return userId ? (
// <Tooltip title={userId}>
// <span>{userId.slice(0, 5)}...</span>
// </Tooltip>
// ) : "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) }
];
@@ -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<Array<{ label: string; value: string }>>;
}
interface FilterValues {
@@ -38,6 +42,9 @@ const FilterComponent: React.FC<FilterComponentProps> = ({
const [filterValues, setFilterValues] = useState<FilterValues>(initialValues);
const [tempValues, setTempValues] = useState<FilterValues>(initialValues);
const [dropdownOpen, setDropdownOpen] = useState<boolean>(false);
const [searchOptions, setSearchOptions] = useState<Array<{ label: string; value: string }>>([]);
const [searchLoading, setSearchLoading] = useState<boolean>(false);
const [searchInputValue, setSearchInputValue] = useState<string>('');
const filtersRef = useRef<HTMLDivElement>(null);
@@ -46,22 +53,40 @@ const FilterComponent: React.FC<FilterComponentProps> = ({
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<FilterComponentProps> = ({
});
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<FilterComponentProps> = ({
),
}));
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 (
<div className="relative" ref={filtersRef}>
<TremorButton
@@ -99,7 +138,6 @@ const FilterComponent: React.FC<FilterComponentProps> = ({
>
{buttonLabel}
</TremorButton>
{showFilters && (
<Card className="absolute left-0 mt-2 w-96 z-50 border border-gray-200 shadow-lg">
<div className="flex flex-col gap-4">
@@ -112,6 +150,7 @@ const FilterComponent: React.FC<FilterComponentProps> = ({
onClick: ({ key }) => {
setSelectedFilter(key);
setDropdownOpen(false);
setSearchOptions([]);
}
}}
onOpenChange={setDropdownOpen}
@@ -119,7 +158,7 @@ const FilterComponent: React.FC<FilterComponentProps> = ({
trigger={['click']}
>
<Button className="min-w-32 text-left flex justify-between items-center">
{selectedFilter}
{currentOption?.label || selectedFilter}
{dropdownOpen ? (
<ChevronUpIcon className="h-4 w-4" />
) : (
@@ -128,40 +167,84 @@ const FilterComponent: React.FC<FilterComponentProps> = ({
</Button>
</Dropdown>
<Input
placeholder="Enter value..."
value={tempValues[selectedFilter] || ''}
onChange={(e) => 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] ? (
<XIcon
className="h-4 w-4 cursor-pointer text-gray-400 hover:text-gray-500"
onClick={(e) => {
e.stopPropagation();
handleFilterChange('');
}}
/>
) : null
{currentOption?.isSearchable ? (
<Select
showSearch
placeholder={`Search ${currentOption.label || selectedFilter}...`}
value={tempValues[selectedFilter] || undefined}
onChange={(value) => handleFilterChange(value)}
onSearch={(value) => {
setSearchInputValue(value);
debouncedSearch(value, currentOption);
}}
onInputKeyDown={(e) => {
if (e.key === 'Enter' && searchInputValue) {
// Allow manual entry of the value on Enter
handleFilterChange(searchInputValue);
e.preventDefault();
}
}}
filterOption={false}
className="flex-1 w-full max-w-full truncate"
loading={searchLoading}
options={searchOptions}
allowClear
notFoundContent={
searchLoading ? <Spin size="small" /> : (
<div className="p-2">
{searchInputValue && (
<Button
type="link"
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();
}
}}
>
Use &ldquo;{searchInputValue}&rdquo; as filter value
</Button>
)}
</div>
)
}
/>
) : (
<Input
placeholder="Enter value..."
value={tempValues[selectedFilter] || ''}
onChange={(e) => 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] ? (
<XIcon
className="h-4 w-4 cursor-pointer text-gray-400 hover:text-gray-500"
onClick={(e) => {
e.stopPropagation();
handleFilterChange('');
}}
/>
) : null
}
/>
)}
</div>
<div className="flex justify-end gap-2">
<div className="flex gap-2 justify-end">
<Button
onClick={() => {
clearFilters();
const emptyValues: FilterValues = {};
options.forEach(option => {
emptyValues[option.name] = '';
});
onResetFilters();
setShowFilters(false);
}}
>
Cancel
Reset
</Button>
<Button type="primary" onClick={handleApplyFilters}>
<Button onClick={handleApplyFilters}>
Apply Filters
</Button>
</div>
@@ -339,7 +339,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({
+ Create New Key
</Button>
<Modal
title="Create Key"
// title="Create Key"
visible={isModalVisible}
width={1000}
footer={null}
@@ -355,7 +355,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({
>
{/* Section 1: Key Ownership */}
<div className="mb-8">
<Title className="mb-4">1. Key Ownership</Title>
<Title className="mb-4">Key Ownership</Title>
<Form.Item
label={
<span>
@@ -444,7 +444,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({
{/* Section 2: Key Details */}
<div className="mb-8">
<Title className="mb-4">2. Key Details</Title>
<Title className="mb-4">Key Details</Title>
<Form.Item
label={
<span>
@@ -503,7 +503,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({
<div className="mb-8">
<Accordion className="mt-4 mb-4">
<AccordionHeader>
<Title className="m-0">3. Optional Settings</Title>
<Title className="m-0">Optional Settings</Title>
</AccordionHeader>
<AccordionBody>
<Form.Item
@@ -0,0 +1,26 @@
import { Organization } from "../networking";
export const createOrgSearchFunction = (organizations: Organization[] | null) => {
return async (searchText: string): Promise<Array<{ label: string; value: string }>> => {
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;
};
};
@@ -0,0 +1,22 @@
import { Team } from "./key_list";
export const createTeamSearchFunction = (teams: Team[] | null) => {
return async (searchText: string): Promise<Array<{ label: string; value: string }>> => {
// 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
}));
};
};