diff --git a/ui/litellm-dashboard/package-lock.json b/ui/litellm-dashboard/package-lock.json index b4c886752df..37027f215fe 100644 --- a/ui/litellm-dashboard/package-lock.json +++ b/ui/litellm-dashboard/package-lock.json @@ -42,6 +42,7 @@ "react-dom": "19.2.8", "react-error-boundary": "6.1.6", "react-hook-form": "7.82.0", + "react-hotkeys-hook": "5.3.3", "react-intersection-observer": "11.0.1", "react-json-view-lite": "2.5.0", "react-markdown": "9.1.0", @@ -10537,6 +10538,19 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-hotkeys-hook": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-5.3.3.tgz", + "integrity": "sha512-aswgyWUnE25hmhzHTfKDmKzsaSE5DJ4LKaU/o6rQSXkDd/1Bh9TfAFQbHkf6fLy11HvlYkp+cDDarGdhmCDhoQ==", + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/react-intersection-observer": { "version": "11.0.1", "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-11.0.1.tgz", diff --git a/ui/litellm-dashboard/package.json b/ui/litellm-dashboard/package.json index bba51e9aa2b..388b6c7eab9 100644 --- a/ui/litellm-dashboard/package.json +++ b/ui/litellm-dashboard/package.json @@ -59,6 +59,7 @@ "react-dom": "19.2.8", "react-error-boundary": "6.1.6", "react-hook-form": "7.82.0", + "react-hotkeys-hook": "5.3.3", "react-intersection-observer": "11.0.1", "react-json-view-lite": "2.5.0", "react-markdown": "9.1.0", diff --git a/ui/litellm-dashboard/src/app/layout.tsx b/ui/litellm-dashboard/src/app/layout.tsx index 60db7104a03..7bb05ef11a7 100644 --- a/ui/litellm-dashboard/src/app/layout.tsx +++ b/ui/litellm-dashboard/src/app/layout.tsx @@ -7,6 +7,7 @@ import { ThemeProvider } from "next-themes"; import { AuthProvider } from "@/contexts/AuthContext"; import ReactQueryProvider from "@/contexts/ReactQueryProvider"; +import HotkeysProvider from "@/contexts/HotkeysProvider"; import { Toaster } from "@/components/ui/sonner"; const inter = Inter({ subsets: ["latin"] }); @@ -30,7 +31,9 @@ export default function RootLayout({ - {children} + + {children} + diff --git a/ui/litellm-dashboard/src/components/lens/HeaderActions.tsx b/ui/litellm-dashboard/src/components/lens/HeaderActions.tsx deleted file mode 100644 index 8df9307c990..00000000000 --- a/ui/litellm-dashboard/src/components/lens/HeaderActions.tsx +++ /dev/null @@ -1,13 +0,0 @@ -"use client"; - -import { useContext, type ReactNode } from "react"; -import { createPortal } from "react-dom"; - -import { LensPreviewContext } from "./LensPreviewButton"; - -export function HeaderActions({ children }: { children: ReactNode }) { - const preview = useContext(LensPreviewContext); - const actions =
{children}
; - if (preview === undefined) return actions; - return preview.target ? createPortal(actions, preview.target) : null; -} diff --git a/ui/litellm-dashboard/src/components/lens/LensModeSwitch.tsx b/ui/litellm-dashboard/src/components/lens/LensModeSwitch.tsx new file mode 100644 index 00000000000..11d02567799 --- /dev/null +++ b/ui/litellm-dashboard/src/components/lens/LensModeSwitch.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { Tabs as TabsPrimitive } from "@base-ui/react/tabs"; +import { Activity, ScanSearch, Settings } from "lucide-react"; +import { StatusDot } from "@/components/shared/StatusDot"; +import { cn } from "@/lib/cva.config"; +import type { InvestigationActivity } from "./model/status"; +import { useWorkerConnected } from "./hooks/useWorkerConnected"; +import type { LensList } from "./model/types"; +import { LENS_TABS, type LensTab } from "./route"; +import { frameCorner, frameTab } from "./ui/frame"; + +const MODE_ICONS = { traces: Activity, investigations: ScanSearch, settings: Settings } as const; + +const ACTIVITY_DOT: Record, { className: string; label: string }> = { + running: { className: "bg-info 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 ( +