diff --git a/ui/litellm-dashboard/src/components/lens/LensWorkspace.integration.test.tsx b/ui/litellm-dashboard/src/components/lens/LensWorkspace.integration.test.tsx index 49aa02fd6f0..cb0547480d1 100644 --- a/ui/litellm-dashboard/src/components/lens/LensWorkspace.integration.test.tsx +++ b/ui/litellm-dashboard/src/components/lens/LensWorkspace.integration.test.tsx @@ -205,4 +205,25 @@ describe("Lens interactive demo", () => { expect(await screen.findByRole("table", { name: "Agent runs" })).toBeVisible(); expect(screen.queryByRole("button", { name: "Preview sample" })).not.toBeInTheDocument(); }); + + it("marks the Investigations tab while a scan runs and clears it once the scan finishes", async () => { + const saved = createLensDemoData().lenses[0]; + const withJob = (status: (typeof saved.jobs)[number]["status"]) => ({ + ...saved, + jobs: [{ ...saved.jobs[0], status }, ...saved.jobs.slice(1)], + }); + const lenses = vi.fn(() => [withJob("running")]); + network.mockImplementation(async (input) => { + const path = new URL(String(input), "http://localhost").pathname; + if (path === "/lens") return Response.json({ lenses: lenses(), workers: [], tracing_enabled: false }); + if (path === "/v1/traces") return Response.json({ detail: "Tracing is not enabled" }, { status: 501 }); + return Response.json({ data: [], traces: false, requests: false }); + }); + renderWithProviders(); + const tab = within(screen.getByRole("tablist", { name: "Lens" })).getByRole("tab", { name: "Investigations" }); + await waitFor(() => expect(tab).toHaveAccessibleDescription("An investigation is running")); + lenses.mockReturnValue([withJob("completed")]); + await testQueryClient.refetchQueries({ queryKey: ["lens", "list"] }); + await waitFor(() => expect(tab).toHaveAccessibleDescription("")); + }); }); diff --git a/ui/litellm-dashboard/src/components/lens/LensWorkspace.tsx b/ui/litellm-dashboard/src/components/lens/LensWorkspace.tsx index 11fdde333b3..5cccb16a99f 100644 --- a/ui/litellm-dashboard/src/components/lens/LensWorkspace.tsx +++ b/ui/litellm-dashboard/src/components/lens/LensWorkspace.tsx @@ -1,8 +1,9 @@ "use client"; import { useId, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; import { Tabs as TabsPrimitive } from "@base-ui/react/tabs"; -import { Activity, Aperture, Info, ScanSearch } from "lucide-react"; +import { Activity, Aperture, ArrowUpRight, Info, ScanSearch } from "lucide-react"; import AgentTracesPage from "@/components/view_logs/TraceView/AgentTracesPage"; import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent } from "@/components/ui/tabs"; @@ -11,6 +12,10 @@ import { LensPreviewContext } from "./LensPreviewButton"; import { isProxyAdminRole, isProxyAdminTierRole } from "@/utils/roles"; import { InvestigationsView } from "./investigations/InvestigationsView"; import { createLensDemo } from "./demo/createLensDemo"; +import { lensQueries } from "./api/queries"; +import { useLensApi } from "./services"; +import { investigationActivity, type InvestigationActivity } from "./model/status"; +import { cn } from "@/lib/cva.config"; import { LENS_TABS, useLensRoute, type LensTab } from "./route"; type WorkspaceProps = { accessToken: string; userRole: string; readOnly: boolean }; @@ -47,79 +52,148 @@ function DemoToggle({ demo, onChange }: { demo: boolean; onChange: (demo: boolea const MODE_ICONS = { traces: Activity, investigations: ScanSearch } as const; -function LensModeSwitch() { +const ACTIVITY_DOT: Record, { className: string; label: string }> = { + running: { className: "bg-[#3b5bfd] motion-safe:animate-pulse", label: "An investigation is running" }, + queued: { className: "bg-muted-foreground/60", label: "An investigation is queued" }, +}; + +function ActivityDot({ activity }: { activity: InvestigationActivity }) { + if (activity === "idle") return null; return ( - - - {Object.entries(LENS_TABS).map(([view, label]) => { - const Icon = MODE_ICONS[view as LensTab]; - return ( - - - ); - })} - +