diff --git a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx index 13e93798def..9462878b875 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx @@ -2,7 +2,7 @@ import React, { Suspense, useEffect, useState } from "react"; import Navbar from "@/components/navbar"; -import { ThemeProvider } from "@/contexts/ThemeContext"; +import { ThemeProvider, useTheme } from "@/contexts/ThemeContext"; import SidebarProvider from "@/app/(dashboard)/components/SidebarProvider"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { useRouter, useSearchParams } from "next/navigation"; @@ -34,10 +34,11 @@ const MIGRATED_PAGES: Record = { "api-reference": "api-reference", }; -function LayoutContent({ children }: { children: React.ReactNode }) { +function DashboardShell({ children }: { children: React.ReactNode }) { const router = useRouter(); const searchParams = useSearchParams(); const { accessToken, userRole, userId, userEmail, premiumUser } = useAuthorized(); + const { isDarkMode, toggleDarkMode } = useTheme(); const [sidebarCollapsed, setSidebarCollapsed] = React.useState(false); const [page, setPage] = useState(() => { return searchParams.get("page") || "api-keys"; @@ -64,34 +65,36 @@ function LayoutContent({ children }: { children: React.ReactNode }) { const toggleSidebar = () => setSidebarCollapsed((v) => !v); return ( - -
- { }} - accessToken={accessToken} - isDarkMode={false} - toggleDarkMode={() => { }} - /> - -
-
- -
-
{children}
+
+ {}} + accessToken={accessToken} + isDarkMode={isDarkMode} + toggleDarkMode={toggleDarkMode} + /> + +
+
+
+
{children}
+
+ ); +} + +function LayoutContent({ children }: { children: React.ReactNode }) { + return ( + + {children} ); } diff --git a/ui/litellm-dashboard/src/app/globals.css b/ui/litellm-dashboard/src/app/globals.css index 0d453e284ef..f5eaa5edbc9 100644 --- a/ui/litellm-dashboard/src/app/globals.css +++ b/ui/litellm-dashboard/src/app/globals.css @@ -9,13 +9,12 @@ --neutral-border: #dcddeb; } -/* @media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } -} */ +.dark { + --foreground-rgb: 229, 231, 235; /* gray-200 */ + --background-start-rgb: 17, 24, 39; /* gray-900 */ + --background-end-rgb: 17, 24, 39; + --neutral-border: #374151; /* gray-700 */ +} body { color: rgb(var(--foreground-rgb)); diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 06bf3b68d05..053b5f81940 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -52,7 +52,6 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { jwtDecode } from "jwt-decode"; import { useRouter, useSearchParams } from "next/navigation"; import { Suspense, useEffect, useMemo, useRef, useState } from "react"; -import { ConfigProvider, theme } from "antd"; function deleteCookie(name: string, path = "/") { // Best-effort client-side clear (works for non-HttpOnly cookies without Domain) @@ -110,12 +109,6 @@ function CreateKeyPageContent() { const [showClaudeCodePrompt, setShowClaudeCodePrompt] = useState(false); const [showClaudeCodeModal, setShowClaudeCodeModal] = useState(false); - // Dark mode state - const [isDarkMode, setIsDarkMode] = useState(false); - const toggleDarkMode = () => { - setIsDarkMode(!isDarkMode); - }; - const invitation_id = searchParams.get("invitation_id"); // Parse URL query parameters for pre-filling the create key form @@ -450,10 +443,7 @@ function CreateKeyPageContent() { return ( }> - - + {invitation_id ? ( ) : ( -
+
@@ -682,7 +670,6 @@ function CreateKeyPageContent() {
)} - ); } diff --git a/ui/litellm-dashboard/src/components/navbar.test.tsx b/ui/litellm-dashboard/src/components/navbar.test.tsx index 2e122164969..1b2cdfd3e20 100644 --- a/ui/litellm-dashboard/src/components/navbar.test.tsx +++ b/ui/litellm-dashboard/src/components/navbar.test.tsx @@ -89,7 +89,11 @@ vi.mock("./Navbar/CommunityEngagementButtons/CommunityEngagementButtons", () => })); // Create mock functions that can be controlled in tests -let mockUseThemeImpl = () => ({ logoUrl: null as string | null }); +let mockUseThemeImpl = () => ({ + logoUrl: null as string | null, + isDarkMode: false, + toggleDarkMode: vi.fn(), +}); let mockUseHealthReadinessDetailsImpl = () => ({ data: null as any }); let mockGetLocalStorageItemImpl = (key: string) => null as string | null; let mockUseAuthorizedImpl = () => ({ @@ -236,7 +240,11 @@ describe("Navbar", () => { }); it("should use custom logo from theme context", () => { - mockUseThemeImpl = () => ({ logoUrl: "https://example.com/custom-logo.png" }); + mockUseThemeImpl = () => ({ + logoUrl: "https://example.com/custom-logo.png", + isDarkMode: false, + toggleDarkMode: vi.fn(), + }); renderWithProviders(); @@ -244,7 +252,7 @@ describe("Navbar", () => { expect(logoImg).toHaveAttribute("src", "https://example.com/custom-logo.png"); // Reset mock - mockUseThemeImpl = () => ({ logoUrl: null }); + mockUseThemeImpl = () => ({ logoUrl: null, isDarkMode: false, toggleDarkMode: vi.fn() }); }); it("should hide user dropdown on public pages", () => { @@ -304,10 +312,38 @@ describe("Navbar", () => { expect(window.location.href).toBe(""); }); - it("should not render dark mode toggle slider", () => { + it("should render dark mode toggle", () => { renderWithProviders(); - // DO NOT RENDER THIS UNTIL ALL COMPONENTS ARE CONFIRMED TO SUPPORT DARK MODE STYLES. IT IS AN ISSUE IF THIS TEST FAILS. - expect(screen.queryByTestId("dark-mode-toggle")).not.toBeInTheDocument(); + expect(screen.getByTestId("dark-mode-toggle")).toBeInTheDocument(); + }); + + it("should call toggleDarkMode when toggle is clicked", async () => { + const user = userEvent.setup(); + const mockToggle = vi.fn(); + renderWithProviders(); + + await user.click(screen.getByTestId("dark-mode-toggle")); + + expect(mockToggle).toHaveBeenCalledTimes(1); + }); + + it("should fall back to ThemeContext when toggle props are not provided", async () => { + const user = userEvent.setup(); + const ctxToggle = vi.fn(); + mockUseThemeImpl = () => ({ + logoUrl: null, + isDarkMode: true, + toggleDarkMode: ctxToggle, + }); + + const { isDarkMode, toggleDarkMode, ...rest } = defaultProps; + renderWithProviders(); + + await user.click(screen.getByTestId("dark-mode-toggle")); + expect(ctxToggle).toHaveBeenCalledTimes(1); + + // Reset mock + mockUseThemeImpl = () => ({ logoUrl: null, isDarkMode: false, toggleDarkMode: vi.fn() }); }); }); diff --git a/ui/litellm-dashboard/src/components/navbar.tsx b/ui/litellm-dashboard/src/components/navbar.tsx index 055038b6d88..45733ea050c 100644 --- a/ui/litellm-dashboard/src/components/navbar.tsx +++ b/ui/litellm-dashboard/src/components/navbar.tsx @@ -25,8 +25,10 @@ interface NavbarProps { isPublicPage: boolean; sidebarCollapsed?: boolean; onToggleSidebar?: () => void; - isDarkMode: boolean; - toggleDarkMode: () => void; + /** Optional override; defaults to the value from ThemeContext. */ + isDarkMode?: boolean; + /** Optional override; defaults to the toggler from ThemeContext. */ + toggleDarkMode?: () => void; } const Navbar: React.FC = ({ @@ -40,12 +42,14 @@ const Navbar: React.FC = ({ isPublicPage = false, sidebarCollapsed = false, onToggleSidebar, - isDarkMode, - toggleDarkMode, + isDarkMode: isDarkModeProp, + toggleDarkMode: toggleDarkModeProp, }) => { const baseUrl = getProxyBaseUrl(); const [logoutUrl, setLogoutUrl] = useState(""); - const { logoUrl } = useTheme(); + const { logoUrl, isDarkMode: isDarkModeCtx, toggleDarkMode: toggleDarkModeCtx } = useTheme(); + const isDarkMode = isDarkModeProp ?? isDarkModeCtx; + const toggleDarkMode = toggleDarkModeProp ?? toggleDarkModeCtx; const { data: healthData } = useHealthReadinessDetails(accessToken); const version = healthData?.litellm_version; const disableBouncingIcon = useDisableBouncingIcon(); @@ -87,14 +91,14 @@ const Navbar: React.FC = ({ }; return ( -