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:
ishaan-berri 2026-10-01 18:41:55 -07:00 • committed by GitHub
parent 309597df49
commit 1eadd7aba0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 57 additions and 36 deletions

View file

@ -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>

View file

@ -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);

View file

@ -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">

View file

@ -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>

View file

@ -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>