From c5c7061c3ecd6d0b8a02cf20e871ebcaf320257b Mon Sep 17 00:00:00 2001 From: gabriele Date: Mon, 5 Oct 2026 10:26:36 +0000 Subject: [PATCH] fix(ui): cap rendered MultiSelect options and stop matching descriptions Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../components/shared/MultiSelect.test.tsx | 60 ++++++++++++++++++- .../src/components/shared/MultiSelect.tsx | 33 +++++++--- 2 files changed, 83 insertions(+), 10 deletions(-) diff --git a/ui/litellm-dashboard/src/components/shared/MultiSelect.test.tsx b/ui/litellm-dashboard/src/components/shared/MultiSelect.test.tsx index cc4084a952e..610c7e225ff 100644 --- a/ui/litellm-dashboard/src/components/shared/MultiSelect.test.tsx +++ b/ui/litellm-dashboard/src/components/shared/MultiSelect.test.tsx @@ -1,13 +1,19 @@ import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; -import { MultiSelect, type MultiSelectOption } from "./MultiSelect"; +import { MAX_VISIBLE_OPTIONS, MultiSelect, type MultiSelectOption } from "./MultiSelect"; const OPTIONS: MultiSelectOption[] = [ { value: "vs-alpha", label: "alpha-kb (vs-alpha)" }, { value: "vs-beta", label: "beta-kb (vs-beta)", description: "second store" }, ]; +const LARGE_OPTIONS: MultiSelectOption[] = Array.from({ length: 5000 }, (_, index) => ({ + value: `tag-${index}`, + label: `tag-${index}`, + description: "shared spend tag description", +})); + const renderMultiSelect = (props: Partial> = {}) => { const onValueChange = vi.fn(); render(); @@ -69,6 +75,58 @@ describe("MultiSelect", () => { expect(screen.getByLabelText("beta-kb (vs-beta)")).toBeInTheDocument(); }); + it("caps rendered options and reports the number of matching options", async () => { + const { input } = renderMultiSelect({ options: LARGE_OPTIONS }); + + await openPopup(input); + + expect(screen.getAllByRole("option")).toHaveLength(MAX_VISIBLE_OPTIONS); + 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 }); + await userEvent.type(input, "shared spend"); + + expect(screen.queryAllByRole("option")).toHaveLength(0); + expect(await screen.findByText("No options found")).toBeInTheDocument(); + }); + + it("filters by label and updates the match hint", async () => { + const { input } = renderMultiSelect({ options: LARGE_OPTIONS }); + await userEvent.type(input, "tag-49"); + + expect(screen.getAllByRole("option")).toHaveLength(MAX_VISIBLE_OPTIONS); + expect(screen.getByText("Showing first 100 of 111 matches. Type to narrow results.")).toBeInTheDocument(); + }); + + it("shows an under-cap label match without the match hint", async () => { + const { input } = renderMultiSelect({ options: LARGE_OPTIONS }); + await userEvent.type(input, "tag-4999"); + + expect(screen.getAllByRole("option")).toHaveLength(1); + expect(screen.getByRole("option", { name: /tag-4999/ })).toBeInTheDocument(); + expect(screen.queryByText(/Showing first/)).not.toBeInTheDocument(); + }); + + it("keeps the custom option available when matching results exceed the cap", async () => { + const { onValueChange, input } = renderMultiSelect({ options: LARGE_OPTIONS, allowCustomValues: true }); + await userEvent.type(input, "tag"); + + await userEvent.click(screen.getByRole("option", { name: 'Create "tag"' })); + + expect(onValueChange).toHaveBeenCalledWith(["tag"]); + }); + + it("renders a selected chip outside the visible option slice", async () => { + const { input } = renderMultiSelect({ options: LARGE_OPTIONS, value: ["tag-4999"] }); + + await openPopup(input); + + expect(screen.getByLabelText("tag-4999")).toBeInTheDocument(); + expect(screen.getAllByRole("option")).toHaveLength(MAX_VISIBLE_OPTIONS); + }); + it("labels an unknown selected value with its raw id", () => { renderMultiSelect({ value: ["vs-deleted"] }); diff --git a/ui/litellm-dashboard/src/components/shared/MultiSelect.tsx b/ui/litellm-dashboard/src/components/shared/MultiSelect.tsx index 3a153ff2a5f..aed8ccec591 100644 --- a/ui/litellm-dashboard/src/components/shared/MultiSelect.tsx +++ b/ui/litellm-dashboard/src/components/shared/MultiSelect.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { Combobox, ComboboxChip, @@ -15,6 +15,8 @@ import { useComboboxAnchor, } from "@/components/ui/combobox"; +export const MAX_VISIBLE_OPTIONS = 100; + export interface MultiSelectOption { label: string; value: string; @@ -46,8 +48,7 @@ const matchesQuery = (option: MultiSelectOption, query: string): boolean => { return ( !normalizedQuery || option.label.toLowerCase().includes(normalizedQuery) || - option.value.toLowerCase().includes(normalizedQuery) || - (option.description?.toLowerCase().includes(normalizedQuery) ?? false) + option.value.toLowerCase().includes(normalizedQuery) ); }; @@ -65,10 +66,19 @@ export function MultiSelect({ }: MultiSelectProps) { const anchor = useComboboxAnchor(); const [query, setQuery] = useState(""); - const safeOptions = options.filter( - (option): option is MultiSelectOption => - option != null && typeof option.value === "string" && option.value.length > 0, + const safeOptions = useMemo( + () => + options.filter( + (option): option is MultiSelectOption => + option != null && typeof option.value === "string" && option.value.length > 0, + ), + [options], ); + const matchingOptions = useMemo( + () => safeOptions.filter((option) => matchesQuery(option, query)), + [safeOptions, query], + ); + const visibleOptions = matchingOptions.slice(0, MAX_VISIBLE_OPTIONS); const selectedOptions = value .filter((selectedValue): selectedValue is string => typeof selectedValue === "string" && selectedValue.length > 0) .map( @@ -82,8 +92,8 @@ export function MultiSelect({ const customOptionExists = safeOptions.some((option) => option.value.toLowerCase() === customOption.toLowerCase()); const items = allowCustomValues && customOption && !customOptionExists - ? [...safeOptions, { label: `Create "${customOption}"`, value: customOption }] - : safeOptions; + ? [{ label: `Create "${customOption}"`, value: customOption }, ...visibleOptions] + : visibleOptions; const canClear = (selected: MultiSelectOption[]) => selected.length > 0 && !disabled && !loading; @@ -99,13 +109,13 @@ export function MultiSelect({ option.value === selected.value} itemToStringLabel={(option: MultiSelectOption) => option.label} - filter={matchesQuery} disabled={disabled || loading} > } className={`min-h-8 py-1 text-sm ${className ?? ""}`}> @@ -142,6 +152,11 @@ export function MultiSelect({ )} + {matchingOptions.length > MAX_VISIBLE_OPTIONS && ( +
+ Showing first {MAX_VISIBLE_OPTIONS} of {matchingOptions.length} matches. Type to narrow results. +
+ )}
);