From 13253896245d58f3c8ae304e04f208efec0ecd33 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Sun, 4 Oct 2026 16:31:28 -0700 Subject: [PATCH] refactor(ui): share the brushed timeline and the state messages The histogram strip only knew about runs through its tooltip copy, and the loading and state messages only lived under Lens. Both move to shared/, the timeline takes the item noun from its caller, and the sweep keyframe drops its Lens name. Co-Authored-By: Claude Fable 5.1 --- ui/litellm-dashboard/src/app/globals.css | 2 +- .../investigations/InvestigationStates.tsx | 4 +-- .../lens/traces/detail/run/RunView.tsx | 2 +- .../AgentTracesSection.integration.test.tsx | 8 ++--- .../lens/traces/list/AgentTracesSection.tsx | 6 ++-- .../lens/traces/list/useTraceHistogram.ts | 8 ++--- .../{lens/ui => shared}/LoadingState.tsx | 0 .../{lens/ui => shared}/StateMessage.tsx | 0 .../timeline/Timeline.test.ts} | 2 +- .../timeline/Timeline.tsx} | 30 +++++++++++-------- 10 files changed, 34 insertions(+), 28 deletions(-) rename ui/litellm-dashboard/src/components/{lens/ui => shared}/LoadingState.tsx (100%) rename ui/litellm-dashboard/src/components/{lens/ui => shared}/StateMessage.tsx (100%) rename ui/litellm-dashboard/src/components/{lens/traces/list/TracesTimeline.test.ts => shared/timeline/Timeline.test.ts} (96%) rename ui/litellm-dashboard/src/components/{lens/traces/list/TracesTimeline.tsx => shared/timeline/Timeline.tsx} (91%) diff --git a/ui/litellm-dashboard/src/app/globals.css b/ui/litellm-dashboard/src/app/globals.css index cfa85817ea6..3de122128d4 100644 --- a/ui/litellm-dashboard/src/app/globals.css +++ b/ui/litellm-dashboard/src/app/globals.css @@ -461,7 +461,7 @@ visibility: hidden; } -@keyframes lens-sweep { +@keyframes timeline-sweep { from { left: -6rem; } diff --git a/ui/litellm-dashboard/src/components/lens/investigations/InvestigationStates.tsx b/ui/litellm-dashboard/src/components/lens/investigations/InvestigationStates.tsx index ead2e0789c1..e41987d3a7f 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/InvestigationStates.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/InvestigationStates.tsx @@ -2,8 +2,8 @@ import { ArrowUpRight, SearchX, TriangleAlert } from "lucide-react"; import { Button, buttonVariants } from "@/components/ui/button"; -import { LoadingState } from "../ui/LoadingState"; -import { StateMessage } from "../ui/StateMessage"; +import { LoadingState } from "@/components/shared/LoadingState"; +import { StateMessage } from "@/components/shared/StateMessage"; import { ApiError } from "@/lib/http/client"; diff --git a/ui/litellm-dashboard/src/components/lens/traces/detail/run/RunView.tsx b/ui/litellm-dashboard/src/components/lens/traces/detail/run/RunView.tsx index b20ed28306b..78fb40d2f06 100644 --- a/ui/litellm-dashboard/src/components/lens/traces/detail/run/RunView.tsx +++ b/ui/litellm-dashboard/src/components/lens/traces/detail/run/RunView.tsx @@ -10,7 +10,7 @@ import { Tabs } from "@/components/ui/tabs"; import { cn } from "@/lib/cva.config"; import { useTracesApi } from "../../api"; -import { LoadingState } from "../../../ui/LoadingState"; +import { LoadingState } from "@/components/shared/LoadingState"; import { classifyTraceReadFailure, traceReadRetry, traceReadRetryDelay } from "../../list/traceReadFailure"; import { type RunSelection, traceKey } from "../../routing"; import type { Trace } from "../../types"; diff --git a/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesSection.integration.test.tsx b/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesSection.integration.test.tsx index a0682897698..e1cf8b49dde 100644 --- a/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesSection.integration.test.tsx +++ b/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesSection.integration.test.tsx @@ -84,7 +84,7 @@ const renderWindowed = (timeControls?: TimeControls) => ); const bucketRunCounts = () => - screen.getAllByTestId("timeline-bucket").map((bucket) => Number(bucket.getAttribute("data-runs"))); + screen.getAllByTestId("timeline-bucket").map((bucket) => Number(bucket.getAttribute("data-total"))); describe("AgentTracesSection", () => { afterEach(() => { @@ -523,10 +523,10 @@ describe("AgentTracesSection", () => { it("plots every loaded run on the timeline", async () => { serve(runs); renderWindowed(); - expect(screen.getByTestId("traces-timeline")).toHaveAttribute("aria-busy", "true"); + expect(screen.getByTestId("timeline")).toHaveAttribute("aria-busy", "true"); await screen.findAllByTestId("agent-trace-row"); - await waitFor(() => expect(screen.getByTestId("traces-timeline")).toHaveAttribute("aria-busy", "false")); + await waitFor(() => expect(screen.getByTestId("timeline")).toHaveAttribute("aria-busy", "false")); const counts = bucketRunCounts(); expect(counts).toHaveLength(60); expect(counts.reduce((a, b) => a + b, 0)).toBe(runs.length); @@ -562,7 +562,7 @@ describe("AgentTracesSection", () => { drag(screen.getByTestId("timeline-selection"), 0, 59); await waitFor(() => expect(rowCount()).toBe(0)); - fireEvent.keyDown(screen.getByTestId("traces-timeline"), { key: "Escape" }); + fireEvent.keyDown(screen.getByTestId("timeline"), { key: "Escape" }); expect(screen.queryByTestId("timeline-selection")).not.toBeInTheDocument(); await waitFor(() => expect(rowCount()).toBe(runs.length)); }); diff --git a/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesSection.tsx b/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesSection.tsx index c82dfb29dfd..e8b33e3d816 100644 --- a/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesSection.tsx +++ b/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesSection.tsx @@ -15,13 +15,14 @@ import { useZoomRouting } from "@/components/shared/timeRange/routing"; import { type RelativeRange, timeWindow } from "@/components/shared/timeRange/timeRange"; import { TimeRangeControls } from "@/components/shared/timeRange/TimeRangeControls"; import type { RelativeRangeState } from "@/components/shared/timeRange/useRelativeRange"; -import { TracesTimeline } from "./TracesTimeline"; +import { Timeline } from "@/components/shared/timeline/Timeline"; import { TracingSetupCard } from "../../onboarding/tracing/TracingSetupCard"; import { useTracesLive } from "../api"; import { type AgentTracesResult, useAgentTraces, useTraceAvailability } from "./useAgentTraces"; import { useTraceHistogram } from "./useTraceHistogram"; const DRAWER_WIDTH_KEY = "litellm.agentTraces.drawerWidth"; +const RUN_NOUN = { singular: "run", plural: "runs" }; export type TimeControls = Pick; @@ -173,12 +174,13 @@ export function AgentTracesSection({ /> )} - +export const toBuckets = (histogram: TraceHistogram): TimeBucket[] => histogram.buckets.map((bucket) => ({ startMs: bucket.start_ms, endMs: bucket.end_ms, @@ -15,7 +15,7 @@ export const toBuckets = (histogram: TraceHistogram): Bucket[] => series: bucket.agents.flatMap(({ agent, runs }) => Array(runs).fill(agent)), })); -const emptyBuckets = (range: TimeWindow): Bucket[] => { +const emptyBuckets = (range: TimeWindow): TimeBucket[] => { const width = (range.endMs - range.startMs) / BUCKETS; return Array.from({ length: BUCKETS }, (_, i) => ({ startMs: range.startMs + i * width, @@ -27,7 +27,7 @@ const emptyBuckets = (range: TimeWindow): Bucket[] => { }; export interface TraceHistogramResult { - buckets: Bucket[]; + buckets: TimeBucket[]; /** True until the first histogram for this scope arrives; a range change keeps the previous one instead. */ isLoading: boolean; } diff --git a/ui/litellm-dashboard/src/components/lens/ui/LoadingState.tsx b/ui/litellm-dashboard/src/components/shared/LoadingState.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/lens/ui/LoadingState.tsx rename to ui/litellm-dashboard/src/components/shared/LoadingState.tsx diff --git a/ui/litellm-dashboard/src/components/lens/ui/StateMessage.tsx b/ui/litellm-dashboard/src/components/shared/StateMessage.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/lens/ui/StateMessage.tsx rename to ui/litellm-dashboard/src/components/shared/StateMessage.tsx diff --git a/ui/litellm-dashboard/src/components/lens/traces/list/TracesTimeline.test.ts b/ui/litellm-dashboard/src/components/shared/timeline/Timeline.test.ts similarity index 96% rename from ui/litellm-dashboard/src/components/lens/traces/list/TracesTimeline.test.ts rename to ui/litellm-dashboard/src/components/shared/timeline/Timeline.test.ts index 8ec8fa69fab..bfb235b09ac 100644 --- a/ui/litellm-dashboard/src/components/lens/traces/list/TracesTimeline.test.ts +++ b/ui/litellm-dashboard/src/components/shared/timeline/Timeline.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { bandForWindow, type Bucket, dragUpdate, formatSpan } from "./TracesTimeline"; +import { bandForWindow, dragUpdate, formatSpan } from "./Timeline"; const HOUR = 3600 * 1000; const START = Date.UTC(2026, 8, 30, 0, 0, 0); diff --git a/ui/litellm-dashboard/src/components/lens/traces/list/TracesTimeline.tsx b/ui/litellm-dashboard/src/components/shared/timeline/Timeline.tsx similarity index 91% rename from ui/litellm-dashboard/src/components/lens/traces/list/TracesTimeline.tsx rename to ui/litellm-dashboard/src/components/shared/timeline/Timeline.tsx index d8ba2b7721a..f3ec2e23258 100644 --- a/ui/litellm-dashboard/src/components/lens/traces/list/TracesTimeline.tsx +++ b/ui/litellm-dashboard/src/components/shared/timeline/Timeline.tsx @@ -19,11 +19,14 @@ const SKELETON_COLUMNS = skeletonColumns(BUCKETS); import type { TimeWindow } from "@/components/shared/timeRange/timeRange"; -export interface Bucket extends DotColumn { +export interface TimeBucket extends DotColumn { startMs: number; endMs: number; } +/** What one counted item is called in the tooltip: "run", "request". */ +export type ItemNoun = { readonly singular: string; readonly plural: string }; + /** Compact window length, Logfire-style: "45m", "6h 12m", "7d", "152d 23h". */ export function formatSpan(ms: number): string { const totalMinutes = Math.max(0, Math.round(ms / MINUTE_MS)); @@ -46,17 +49,18 @@ const tickShift = (t: number): string => { return "translateX(-50%)"; }; -interface TracesTimelineProps { - buckets: readonly Bucket[]; +interface TimelineProps { + buckets: readonly TimeBucket[]; range: TimeWindow; selection: TimeWindow | null; onSelect: (selection: TimeWindow | null) => void; + noun: ItemNoun; /** Paint a muted placeholder profile until the first histogram arrives. */ loading?: boolean; } -function BucketBar({ bucket }: { bucket: Bucket }) { - return
; +function BucketBar({ bucket }: { bucket: TimeBucket }) { + return
; } function NowEdge() { @@ -64,7 +68,7 @@ function NowEdge() { <>