diff --git a/ui/litellm-dashboard/src/components/add_model/advanced_settings.tsx b/ui/litellm-dashboard/src/components/add_model/advanced_settings.tsx index eb12e49ca89..1ad2d132135 100644 --- a/ui/litellm-dashboard/src/components/add_model/advanced_settings.tsx +++ b/ui/litellm-dashboard/src/components/add_model/advanced_settings.tsx @@ -193,6 +193,7 @@ const AdvancedSettings: React.FC = ({ label: tag.name, description: tag.description || undefined, }))} + searchDescriptions={false} allowCustomValues /> )} diff --git a/ui/litellm-dashboard/src/components/shared/MultiSelect.test.tsx b/ui/litellm-dashboard/src/components/shared/MultiSelect.test.tsx index 610c7e225ff..7d411b7c006 100644 --- a/ui/litellm-dashboard/src/components/shared/MultiSelect.test.tsx +++ b/ui/litellm-dashboard/src/components/shared/MultiSelect.test.tsx @@ -84,8 +84,16 @@ describe("MultiSelect", () => { expect(screen.getByText("Showing first 100 of 5000 matches. Type to narrow results.")).toBeInTheDocument(); }); - it("does not match the option description", async () => { - const { input } = renderMultiSelect({ options: LARGE_OPTIONS }); + it("matches option descriptions by default", async () => { + const { input } = renderMultiSelect(); + await userEvent.type(input, "second store"); + + expect(screen.getAllByRole("option")).toHaveLength(1); + expect(screen.getByRole("option", { name: /beta-kb/ })).toBeInTheDocument(); + }); + + it("skips descriptions when searchDescriptions is false", async () => { + const { input } = renderMultiSelect({ options: LARGE_OPTIONS, searchDescriptions: false }); await userEvent.type(input, "shared spend"); expect(screen.queryAllByRole("option")).toHaveLength(0); diff --git a/ui/litellm-dashboard/src/components/shared/MultiSelect.tsx b/ui/litellm-dashboard/src/components/shared/MultiSelect.tsx index aed8ccec591..e7c85615324 100644 --- a/ui/litellm-dashboard/src/components/shared/MultiSelect.tsx +++ b/ui/litellm-dashboard/src/components/shared/MultiSelect.tsx @@ -34,6 +34,7 @@ interface MultiSelectProps { disabled?: boolean; loading?: boolean; allowCustomValues?: boolean; + searchDescriptions?: boolean; className?: string; } @@ -43,13 +44,13 @@ const splitOnCommas = (raw: string): string[] => .map((part) => part.trim()) .filter((part) => part.length > 0); -const matchesQuery = (option: MultiSelectOption, query: string): boolean => { +const matchesQuery = (option: MultiSelectOption, query: string, searchDescriptions: boolean): boolean => { const normalizedQuery = query.trim().toLowerCase(); - return ( - !normalizedQuery || - option.label.toLowerCase().includes(normalizedQuery) || - option.value.toLowerCase().includes(normalizedQuery) - ); + const matchesLabelOrValue = + option.label.toLowerCase().includes(normalizedQuery) || option.value.toLowerCase().includes(normalizedQuery); + const matchesDescription = + searchDescriptions && (option.description?.toLowerCase().includes(normalizedQuery) ?? false); + return !normalizedQuery || matchesLabelOrValue || matchesDescription; }; export function MultiSelect({ @@ -62,6 +63,7 @@ export function MultiSelect({ disabled = false, loading = false, allowCustomValues = false, + searchDescriptions = true, className, }: MultiSelectProps) { const anchor = useComboboxAnchor(); @@ -75,8 +77,8 @@ export function MultiSelect({ [options], ); const matchingOptions = useMemo( - () => safeOptions.filter((option) => matchesQuery(option, query)), - [safeOptions, query], + () => safeOptions.filter((option) => matchesQuery(option, query, searchDescriptions)), + [safeOptions, query, searchDescriptions], ); const visibleOptions = matchingOptions.slice(0, MAX_VISIBLE_OPTIONS); const selectedOptions = value diff --git a/ui/litellm-dashboard/src/components/tag_management/TagSelector.test.tsx b/ui/litellm-dashboard/src/components/tag_management/TagSelector.test.tsx index 4b02dffa406..2672d32ac7a 100644 --- a/ui/litellm-dashboard/src/components/tag_management/TagSelector.test.tsx +++ b/ui/litellm-dashboard/src/components/tag_management/TagSelector.test.tsx @@ -1,8 +1,21 @@ -import { fireEvent, render } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { tagListCall } from "../networking"; import TagSelector from "./TagSelector"; +vi.mock("../networking", () => ({ + tagListCall: vi.fn(), +})); + +const mockTagListCall = vi.mocked(tagListCall); + describe("TagSelector", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockTagListCall.mockResolvedValue({}); + }); + it("should render the tag selector", () => { render( {}} accessToken="test-token" />); }); @@ -18,4 +31,35 @@ describe("TagSelector", () => { expect(tagSelector).toHaveValue("new-tag"); } }); + + it("does not match shared dynamic tag descriptions", async () => { + mockTagListCall.mockResolvedValue({ + first: { + name: "dynamic-tag-one", + description: "This is just a spend tag that was passed dynamically", + models: [], + created_at: "2025-01-01", + updated_at: "2025-01-01", + }, + second: { + name: "dynamic-tag-two", + description: "This is just a spend tag that was passed dynamically", + models: [], + created_at: "2025-01-01", + updated_at: "2025-01-01", + }, + }); + const user = userEvent.setup(); + render( {}} accessToken="test-token" />); + + await waitFor(() => expect(mockTagListCall).toHaveBeenCalledWith("test-token")); + const input = screen.getByRole("combobox"); + await waitFor(() => expect(input).toBeEnabled()); + await user.click(input); + expect(await screen.findByRole("option", { name: /dynamic-tag-one/ })).toBeInTheDocument(); + await user.type(input, "spend tag"); + + expect(await screen.findByRole("option", { name: 'Create "spend tag"' })).toBeInTheDocument(); + expect(screen.getAllByRole("option")).toHaveLength(1); + }); }); diff --git a/ui/litellm-dashboard/src/components/tag_management/TagSelector.tsx b/ui/litellm-dashboard/src/components/tag_management/TagSelector.tsx index 7ca809a0610..cbacbf0d761 100644 --- a/ui/litellm-dashboard/src/components/tag_management/TagSelector.tsx +++ b/ui/litellm-dashboard/src/components/tag_management/TagSelector.tsx @@ -39,6 +39,7 @@ const TagSelector: React.FC = ({ onChange, value, className, a loading={loading} className={className} allowCustomValues + searchDescriptions={false} options={tags.map((tag) => ({ label: tag.name, value: tag.name,