mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
fix(ui): label lens trace services as agents (#44116)
* fix(ui): label the lens trace service filter as agent * fix(ui): rename the lens traces service column to agent * refactor(ui): name the lens trace filter state after agents * test(ui): cover the agent column and filter on lens traces * style(ui): format lens runs toolbar with prettier Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(ui): name the lens selection footer condition Keeps local/no-long-condition-chain within its eslint budget on main Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
309597df49
commit
1eadd7aba0
5 changed files with 57 additions and 36 deletions
|
|
@ -417,6 +417,7 @@ function MatchingActivity({
|
|||
}) {
|
||||
const paginated = data?.next_offset != null || offset > 0;
|
||||
const showSelection = selectedCount !== data?.eligible || paginated;
|
||||
const selectionData = ready && showSelection ? data : undefined;
|
||||
return (
|
||||
<section aria-label="Matching activity" className="self-start rounded-lg border">
|
||||
<div className="border-b px-4 py-3">
|
||||
|
|
@ -473,15 +474,15 @@ function MatchingActivity({
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
{ready && data && showSelection && (
|
||||
{selectionData && (
|
||||
<div className="border-t px-4 py-3 space-y-2">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{selectedCount} selected for analysis
|
||||
{paginated && (
|
||||
<>
|
||||
{" "}
|
||||
· Showing {offset + (data.executions.length ? 1 : 0)}–{offset + data.executions.length} of{" "}
|
||||
{data.eligible}
|
||||
· Showing {offset + (selectionData.executions.length ? 1 : 0)}–
|
||||
{offset + selectionData.executions.length} of {selectionData.eligible}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
|
|
@ -498,8 +499,8 @@ function MatchingActivity({
|
|||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
disabled={data.next_offset == null}
|
||||
onClick={() => onPage(data.next_offset ?? offset)}
|
||||
disabled={selectionData.next_offset == null}
|
||||
onClick={() => onPage(selectionData.next_offset ?? offset)}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { act, fireEvent, screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { ApiError } from "@/lib/http/client";
|
||||
|
||||
import { renderWithProviders, testQueryClient } from "../../../../tests/test-utils";
|
||||
import { chooseSelectOption, renderWithProviders, testQueryClient } from "../../../../tests/test-utils";
|
||||
import traceList from "./__fixtures__/trace_list.json";
|
||||
import AgentTracesPage from "./AgentTracesPage";
|
||||
import { AgentTracesSection, filterRuns } from "./AgentTracesSection";
|
||||
|
|
@ -269,6 +270,28 @@ describe("AgentTracesSection", () => {
|
|||
expect(rows[0]).toHaveTextContent("Should we store OTEL agent spans");
|
||||
});
|
||||
|
||||
it("labels the OTEL service as the agent and filters runs by it", async () => {
|
||||
vi.mocked(agentTraceListCall).mockResolvedValue({
|
||||
...(traceList as TracePage),
|
||||
data: [...runs.slice(1), { ...runs[0], service: "billing-agent" }],
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
renderSection();
|
||||
await screen.findAllByTestId("agent-trace-row");
|
||||
|
||||
expect(screen.getByRole("columnheader", { name: "Agent" })).toBeInTheDocument();
|
||||
const agentFilter = screen.getByRole("combobox", { name: "Filter by agent" });
|
||||
expect(agentFilter).toHaveTextContent("All agents");
|
||||
|
||||
await chooseSelectOption(user, agentFilter, "billing-agent");
|
||||
const rows = screen.getAllByTestId("agent-trace-row");
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]).toHaveTextContent("billing-agent");
|
||||
|
||||
await chooseSelectOption(user, agentFilter, "All agents");
|
||||
expect(screen.getAllByTestId("agent-trace-row")).toHaveLength(runs.length);
|
||||
});
|
||||
|
||||
it("status filter 'Failed' keeps only runs with errors", () => {
|
||||
const failed = filterRuns(runs, "", "all", "error");
|
||||
expect(failed.length).toBeGreaterThan(0);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { Button } from "@/components/ui/button";
|
|||
|
||||
import { AgentTracesTable } from "./AgentTracesTable";
|
||||
import { RunDrawer } from "./RunDrawer";
|
||||
import { ALL_SERVICES, RunsToolbar, type RunStatusFilter } from "./RunsToolbar";
|
||||
import { ALL_AGENTS, RunsToolbar, type RunStatusFilter } from "./RunsToolbar";
|
||||
import type { TraceSummary } from "./traceTypes";
|
||||
import { previewText } from "./traceUtils";
|
||||
import { TimeRangeControls } from "./TimeRangeControls";
|
||||
|
|
@ -16,21 +16,21 @@ import { ActiveDot } from "./ActiveDot";
|
|||
import { TracingSetupCard } from "./TracingSetupCard";
|
||||
import { type AgentTracesResult, traceWindowStartMs, useAgentTraces, useTraceAvailability } from "./useAgentTraces";
|
||||
|
||||
/** Client-side search (input text or trace id) plus service / status filters over the loaded runs. */
|
||||
/** Client-side search (input text or trace id) plus agent / status filters over the loaded runs. */
|
||||
export function filterRuns(
|
||||
runs: TraceSummary[],
|
||||
query: string,
|
||||
service: string,
|
||||
agent: string,
|
||||
status: RunStatusFilter,
|
||||
): TraceSummary[] {
|
||||
const q = query.trim().toLowerCase();
|
||||
return runs.filter((run) => {
|
||||
const haystack = [run.trace_id, previewText(run.input_preview), run.name].map((s) => s.toLowerCase());
|
||||
const matchesQuery = !q || haystack.some((text) => text.includes(q));
|
||||
const matchesService = service === ALL_SERVICES || run.service === service;
|
||||
const matchesAgent = agent === ALL_AGENTS || run.service === agent;
|
||||
const failed = run.error_count > 0;
|
||||
const matchesStatus = status === "all" || (status === "error" ? failed : !failed);
|
||||
return matchesQuery && matchesService && matchesStatus;
|
||||
return matchesQuery && matchesAgent && matchesStatus;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -105,7 +105,7 @@ export function AgentTracesSection({
|
|||
}: AgentTracesSectionProps) {
|
||||
const [openTrace, setOpenTrace] = useState<TraceSummary | null>(null);
|
||||
const [query, setQuery] = useState("");
|
||||
const [service, setService] = useState(ALL_SERVICES);
|
||||
const [agent, setAgent] = useState(ALL_AGENTS);
|
||||
const [status, setStatus] = useState<RunStatusFilter>("all");
|
||||
const [showSetup, setShowSetup] = useState(false);
|
||||
const [zoom, setZoom] = useState<TimeWindow | null>(null);
|
||||
|
|
@ -121,7 +121,7 @@ export function AgentTracesSection({
|
|||
if (setup.disabledDetail == null) void history.refetch();
|
||||
};
|
||||
|
||||
const services = useMemo(() => Array.from(new Set(traces.traces.map((t) => t.service))).sort(), [traces.traces]);
|
||||
const agents = useMemo(() => Array.from(new Set(traces.traces.map((t) => t.service))).sort(), [traces.traces]);
|
||||
// Relative ranges end "now" (the list query uses Date.now() too); round to the minute so the histogram is stable.
|
||||
const endMs = isCustomDate ? moment(endTime).valueOf() : moment().endOf("minute").valueOf();
|
||||
const range = useMemo(
|
||||
|
|
@ -129,8 +129,8 @@ export function AgentTracesSection({
|
|||
[startTime, endTime, isCustomDate, endMs],
|
||||
);
|
||||
const filtered = useMemo(
|
||||
() => filterRuns(traces.traces, query, service, status),
|
||||
[traces.traces, query, service, status],
|
||||
() => filterRuns(traces.traces, query, agent, status),
|
||||
[traces.traces, query, agent, status],
|
||||
);
|
||||
const runs = useMemo(() => (zoom ? filterByWindow(filtered, zoom) : filtered), [filtered, zoom]);
|
||||
|
||||
|
|
@ -193,11 +193,11 @@ export function AgentTracesSection({
|
|||
<RunDrawer trace={openTrace} runs={runs} accessToken={accessToken} onSelect={openRun} />
|
||||
<RunsToolbar
|
||||
query={query}
|
||||
service={service}
|
||||
agent={agent}
|
||||
status={status}
|
||||
services={services}
|
||||
agents={agents}
|
||||
onQueryChange={setQuery}
|
||||
onServiceChange={setService}
|
||||
onAgentChange={setAgent}
|
||||
onStatusChange={setStatus}
|
||||
>
|
||||
<Button variant="outline" size="sm" onClick={() => setShowSetup(true)} className="shrink-0 gap-1.5">
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export function AgentTracesTable({
|
|||
Time <ArrowDown className="size-2.5" />
|
||||
</span>
|
||||
</th>
|
||||
<th className={`w-[160px] ${TH}`}>Service</th>
|
||||
<th className={`w-[160px] ${TH}`}>Agent</th>
|
||||
<th className={TH}>Input</th>
|
||||
<th className={`w-[72px] ${TH_NUM}`}>Agents</th>
|
||||
<th className={`w-[74px] ${TH_NUM}`}>Steps</th>
|
||||
|
|
|
|||
|
|
@ -7,15 +7,15 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
|||
|
||||
export type RunStatusFilter = "all" | "ok" | "error";
|
||||
|
||||
export const ALL_SERVICES = "all";
|
||||
export const ALL_AGENTS = "all";
|
||||
|
||||
interface RunsToolbarProps {
|
||||
query: string;
|
||||
service: string;
|
||||
agent: string;
|
||||
status: RunStatusFilter;
|
||||
services: string[];
|
||||
agents: string[];
|
||||
onQueryChange: (value: string) => void;
|
||||
onServiceChange: (value: string) => void;
|
||||
onAgentChange: (value: string) => void;
|
||||
onStatusChange: (value: RunStatusFilter) => void;
|
||||
/** Extra controls (time range, live tail) rendered on the right. */
|
||||
children?: React.ReactNode;
|
||||
|
|
@ -27,21 +27,18 @@ const STATUS_ITEMS: { value: RunStatusFilter; label: string }[] = [
|
|||
{ value: "error", label: "Failed" },
|
||||
];
|
||||
|
||||
/** Search + service / status filters for the Runs table. Filtering is client-side over the loaded page. */
|
||||
/** Search + agent / status filters for the Runs table. Filtering is client-side over the loaded page. */
|
||||
export function RunsToolbar({
|
||||
query,
|
||||
service,
|
||||
agent,
|
||||
status,
|
||||
services,
|
||||
agents,
|
||||
onQueryChange,
|
||||
onServiceChange,
|
||||
onAgentChange,
|
||||
onStatusChange,
|
||||
children,
|
||||
}: RunsToolbarProps) {
|
||||
const serviceItems = [
|
||||
{ value: ALL_SERVICES, label: "All services" },
|
||||
...services.map((s) => ({ value: s, label: s })),
|
||||
];
|
||||
const agentItems = [{ value: ALL_AGENTS, label: "All agents" }, ...agents.map((s) => ({ value: s, label: s }))];
|
||||
return (
|
||||
<div className="flex min-h-11 shrink-0 flex-wrap items-center gap-2 border-b border-border bg-card px-3 py-2">
|
||||
<div className="relative w-full max-w-[380px]">
|
||||
|
|
@ -55,15 +52,15 @@ export function RunsToolbar({
|
|||
/>
|
||||
</div>
|
||||
<Select
|
||||
items={serviceItems}
|
||||
value={service}
|
||||
onValueChange={(value: string | null) => value !== null && onServiceChange(value)}
|
||||
items={agentItems}
|
||||
value={agent}
|
||||
onValueChange={(value: string | null) => value !== null && onAgentChange(value)}
|
||||
>
|
||||
<SelectTrigger size="sm" className="h-7 min-w-[130px] text-[12px]" aria-label="Filter by service">
|
||||
<SelectTrigger size="sm" className="h-7 min-w-[130px] text-[12px]" aria-label="Filter by agent">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{serviceItems.map((item) => (
|
||||
{agentItems.map((item) => (
|
||||
<SelectItem key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue