From 21881c571181fc0e409dd717b8a277e5b43152a7 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 16:11:52 +0000 Subject: [PATCH] refactor(ui): extract shared timeline and time-range controls (#44584) Move the timeline renderer to shared/timeline/Timeline taking buckets, a selected window, and callbacks as props, and TimeRangeControls to shared/timeline. Lens keeps bucketRuns as the adapter that converts loaded traces into buckets, preserving behavior without the histogram endpoint. Co-authored-by: Yujong Lee Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../lens/traces/list/AgentTracesSection.tsx | 5 +- .../lens/traces/list/TracesTimeline.test.ts | 42 +-- .../lens/traces/list/TracesTimeline.tsx | 318 +---------------- .../lens/traces/list/runSearch/RunSearch.tsx | 2 +- .../traces/list/runSearch/RunsToolbar.tsx | 2 +- .../lens/traces/list/runSearch/runSql.ts | 2 +- .../src/components/lens/traces/routing.ts | 4 +- .../timeline}/TimeRangeControls.tsx | 2 +- .../shared/timeline/Timeline.test.ts | 55 +++ .../components/shared/timeline/Timeline.tsx | 332 ++++++++++++++++++ 10 files changed, 402 insertions(+), 362 deletions(-) rename ui/litellm-dashboard/src/components/{lens/traces/list => shared/timeline}/TimeRangeControls.tsx (98%) create mode 100644 ui/litellm-dashboard/src/components/shared/timeline/Timeline.test.ts create mode 100644 ui/litellm-dashboard/src/components/shared/timeline/Timeline.tsx 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 ffdb3a959a5..4f18584d9ce 100644 --- a/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesSection.tsx +++ b/ui/litellm-dashboard/src/components/lens/traces/list/AgentTracesSection.tsx @@ -19,8 +19,9 @@ import { } from "../routing"; import type { TraceSummary } from "../types"; import { RunView } from "../detail/run/RunView"; -import { TimeRangeControls } from "./TimeRangeControls"; -import { TracesTimeline, type TimeWindow } from "./TracesTimeline"; +import { TimeRangeControls } from "@/components/shared/timeline/TimeRangeControls"; +import { TracesTimeline } from "./TracesTimeline"; +import type { TimeWindow } from "@/components/shared/timeline/Timeline"; import { TracingSetupCard } from "../../onboarding/tracing/TracingSetupCard"; import { useTracesLive } from "../api"; import { type AgentTracesResult, traceWindowStartMs, useAgentTraces, useTraceAvailability } from "./useAgentTraces"; diff --git a/ui/litellm-dashboard/src/components/lens/traces/list/TracesTimeline.test.ts b/ui/litellm-dashboard/src/components/lens/traces/list/TracesTimeline.test.ts index 4da827ae370..583f5886f5a 100644 --- a/ui/litellm-dashboard/src/components/lens/traces/list/TracesTimeline.test.ts +++ b/ui/litellm-dashboard/src/components/lens/traces/list/TracesTimeline.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { bandForWindow, bucketRuns, dragUpdate, formatSpan } from "./TracesTimeline"; +import { bucketRuns } from "./TracesTimeline"; import type { TraceSummary } from "../types"; const HOUR = 3600 * 1000; @@ -41,43 +41,3 @@ describe("bucketRuns", () => { expect(buckets[5]).toMatchObject({ total: 1, failed: 0 }); }); }); - -describe("formatSpan", () => { - it("prints the largest two units, dropping zero parts", () => { - expect(formatSpan(45 * 60 * 1000)).toBe("45m"); - expect(formatSpan(6 * HOUR + 12 * 60 * 1000)).toBe("6h 12m"); - expect(formatSpan(24 * HOUR)).toBe("1d"); - expect(formatSpan(152 * 24 * HOUR + 23 * HOUR)).toBe("152d 23h"); - expect(formatSpan(-5)).toBe("0m"); - }); -}); - -describe("dragUpdate", () => { - const band = { lo: 10, hi: 14 }; - - it("selects between the press point and the pointer, in either direction", () => { - expect(dragUpdate({ mode: "select", origin: 20, band: { lo: 20, hi: 20 } }, 25)).toEqual({ lo: 20, hi: 25 }); - expect(dragUpdate({ mode: "select", origin: 20, band: { lo: 20, hi: 20 } }, 12)).toEqual({ lo: 12, hi: 20 }); - }); - - it("resizes one edge without letting it cross the other", () => { - expect(dragUpdate({ mode: "resize-lo", origin: 10, band }, 4)).toEqual({ lo: 4, hi: 14 }); - expect(dragUpdate({ mode: "resize-lo", origin: 10, band }, 30)).toEqual({ lo: 14, hi: 14 }); - expect(dragUpdate({ mode: "resize-hi", origin: 14, band }, 40)).toEqual({ lo: 10, hi: 40 }); - expect(dragUpdate({ mode: "resize-hi", origin: 14, band }, 2)).toEqual({ lo: 10, hi: 10 }); - }); - - it("pans the band keeping its width, clamped to the strip", () => { - expect(dragUpdate({ mode: "move", origin: 12, band }, 20)).toEqual({ lo: 18, hi: 22 }); - expect(dragUpdate({ mode: "move", origin: 12, band }, -50)).toEqual({ lo: 0, hi: 4 }); - expect(dragUpdate({ mode: "move", origin: 12, band }, 500, 60)).toEqual({ lo: 55, hi: 59 }); - }); -}); - -describe("bandForWindow", () => { - it("maps a selected window back to the buckets it covers", () => { - const buckets = bucketRuns([], range, 10); - expect(bandForWindow(buckets, { startMs: START + 2 * HOUR, endMs: START + 5 * HOUR })).toEqual({ lo: 2, hi: 4 }); - expect(bandForWindow(buckets, null)).toBeNull(); - }); -}); diff --git a/ui/litellm-dashboard/src/components/lens/traces/list/TracesTimeline.tsx b/ui/litellm-dashboard/src/components/lens/traces/list/TracesTimeline.tsx index 0cc580152fe..9cf947719d3 100644 --- a/ui/litellm-dashboard/src/components/lens/traces/list/TracesTimeline.tsx +++ b/ui/litellm-dashboard/src/components/lens/traces/list/TracesTimeline.tsx @@ -1,36 +1,15 @@ "use client"; -import { X } from "lucide-react"; import moment from "moment"; -import { useMemo, useRef, useState, type RefObject } from "react"; -import { useResizeObserver } from "usehooks-ts"; +import { useMemo } from "react"; -import { DotFieldCanvas, DotFieldRoot } from "@/components/shared/dotField/DotField"; -import type { DotBand, DotColumn } from "@/components/shared/dotField/dots"; -import { cn } from "@/lib/cva.config"; +import { Timeline, TIMELINE_BUCKETS, type Bucket, type TimeWindow } from "@/components/shared/timeline/Timeline"; import type { TraceSummary } from "../types"; import { traceAgentNames } from "../utils"; -const BUCKETS = 60; -const TICKS = 6; -const MINUTE_MS = 60 * 1000; -const HOUR_MS = 60 * MINUTE_MS; -const DAY_MS = 24 * HOUR_MS; -const EDGE_FORMAT = "MMM DD, HH:mm"; - -export interface TimeWindow { - startMs: number; - endMs: number; -} - -export interface Bucket extends DotColumn { - startMs: number; - endMs: number; -} - /** Run counts per equal-width time bucket across the window; runs outside it are dropped. */ -export function bucketRuns(runs: readonly TraceSummary[], range: TimeWindow, buckets = BUCKETS): Bucket[] { +export function bucketRuns(runs: readonly TraceSummary[], range: TimeWindow, buckets = TIMELINE_BUCKETS): Bucket[] { const width = (range.endMs - range.startMs) / buckets; const placed = runs.map((run) => ({ index: Math.floor((moment(run.start_time).valueOf() - range.startMs) / width), @@ -49,28 +28,6 @@ export function bucketRuns(runs: readonly TraceSummary[], range: TimeWindow, buc }); } -/** 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)); - const days = Math.floor(totalMinutes / (24 * 60)); - const hours = Math.floor((totalMinutes % (24 * 60)) / 60); - const minutes = totalMinutes % 60; - if (days > 0) return hours > 0 ? `${days}d ${hours}h` : `${days}d`; - if (hours > 0) return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`; - return `${minutes}m`; -} - -const tickFormat = (range: TimeWindow): string => (range.endMs - range.startMs > 2 * DAY_MS ? EDGE_FORMAT : "HH:mm"); - -const pct = (value: number): string => `${value * 100}%`; - -/** Keep the first / last tick label inside the strip; center the rest on their tick. */ -const tickShift = (t: number): string => { - if (t === 0) return "translateX(0)"; - if (t === 1) return "translateX(-100%)"; - return "translateX(-50%)"; -}; - interface TracesTimelineProps { runs: readonly TraceSummary[]; range: TimeWindow; @@ -78,273 +35,8 @@ interface TracesTimelineProps { onSelect: (selection: TimeWindow | null) => void; } -function BucketBar({ bucket }: { bucket: Bucket }) { - return
; -} - -function NowEdge() { - return ( - <> -