diff --git a/ui/litellm-dashboard/src/components/lens/traces/list/runSearch/RunsToolbar.test.tsx b/ui/litellm-dashboard/src/components/lens/traces/list/runSearch/RunsToolbar.test.tsx new file mode 100644 index 00000000000..4b5285b6513 --- /dev/null +++ b/ui/litellm-dashboard/src/components/lens/traces/list/runSearch/RunsToolbar.test.tsx @@ -0,0 +1,55 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { withNuqsTestingAdapter, type UrlUpdateEvent } from "nuqs/adapters/testing"; +import { describe, expect, it, vi } from "vitest"; + +import { runs } from "./__fixtures__/runs"; +import { RunsToolbar } from "./RunsToolbar"; + +const agentBox = () => screen.getByRole("combobox", { name: "Filter traces by agent" }); + +describe("RunsToolbar", () => { + it("narrows the agent filter as you type and selects the match", async () => { + const user = userEvent.setup(); + const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>(); + render(, { + wrapper: withNuqsTestingAdapter({ onUrlUpdate }), + }); + expect(agentBox()).toHaveAttribute("placeholder", "All agents"); + await user.click(agentBox()); + await user.keyboard("tri"); + expect(screen.getAllByRole("option").map((o) => o.textContent)).toEqual(["triage"]); + await user.click(screen.getByRole("option", { name: "triage" })); + expect(onUrlUpdate.mock.lastCall?.[0].searchParams.get("agent")).toBe("triage"); + }); + + it("picks the first match on Enter", async () => { + const user = userEvent.setup(); + const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>(); + render(, { + wrapper: withNuqsTestingAdapter({ onUrlUpdate }), + }); + await user.click(agentBox()); + await user.keyboard("res{Enter}"); + expect(onUrlUpdate.mock.lastCall?.[0].searchParams.get("agent")).toBe("researcher"); + }); + + it("says when no agent matches", async () => { + const user = userEvent.setup(); + render(, { wrapper: withNuqsTestingAdapter() }); + await user.click(agentBox()); + await user.keyboard("zzz"); + expect(screen.getByText("No matching agents")).toBeVisible(); + }); + + it("clears back to all agents", async () => { + const user = userEvent.setup(); + const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>(); + render(, { + wrapper: withNuqsTestingAdapter({ searchParams: "?agent=triage", onUrlUpdate }), + }); + expect(agentBox()).toHaveValue("triage"); + await user.click(screen.getByRole("button", { name: "Clear" })); + expect(onUrlUpdate.mock.lastCall?.[0].searchParams.get("agent")).toBeNull(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/lens/traces/list/runSearch/RunsToolbar.tsx b/ui/litellm-dashboard/src/components/lens/traces/list/runSearch/RunsToolbar.tsx index 67357e70a30..cdf0f1a34a3 100644 --- a/ui/litellm-dashboard/src/components/lens/traces/list/runSearch/RunsToolbar.tsx +++ b/ui/litellm-dashboard/src/components/lens/traces/list/runSearch/RunsToolbar.tsx @@ -3,6 +3,14 @@ import type { TraceSummary } from "../../types"; import type { TimeWindow } from "@/components/shared/timeline/Timeline"; +import { + Combobox, + ComboboxContent, + ComboboxEmpty, + ComboboxInput, + ComboboxItem, + ComboboxList, +} from "@/components/ui/combobox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { useRunFilterRouting } from "../../routing"; import { traceAgentNames } from "../../utils"; @@ -20,12 +28,7 @@ interface RunsToolbarProps { export function RunsToolbar({ query, onQueryChange, runs, range, children }: RunsToolbarProps) { const { agent, status, setAgent, setStatus } = useRunFilterRouting(); - const agents = [ - { value: "", label: "All agents" }, - ...[...new Set([...runs.flatMap(traceAgentNames), ...(agent ? [agent] : [])])] - .sort() - .map((name) => ({ value: name, label: name })), - ]; + const agents = [...new Set([...runs.flatMap(traceAgentNames), ...(agent ? [agent] : [])])].sort(); const statuses = [ { value: "all", label: "All status" }, { value: "ok", label: "No errors" }, @@ -34,18 +37,24 @@ export function RunsToolbar({ query, onQueryChange, runs, range, children }: Run return (
- + setAgent(name ?? "")} autoHighlight> + + + No matching agents + + {(name: string) => ( + + {name} + + )} + + +