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() { <>