feat(ui): migrate navbar + ui_theme_settings to shadcn

- navbar: antd Button/Switch/Tag + @ant-design/icons Menu/PanelLeftClose/
  Moon/Sun \u2192 shadcn Button/Switch/Badge + lucide equivalents. Docs link
  now uses <Button variant='ghost' asChild><a/></Button>. Version Badge
  uses variant='outline'.
- ui_theme_settings: @tremor Card/Title/Text/TextInput/Button \u2192 shadcn
  Card/Input/Button/Label. Semantic tokens throughout (text-foreground,
  text-muted-foreground, bg-background).

Gates: TS \u2713 | Build \u2713.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 10:32:27 +00:00
parent 32392518f6
commit 1c27c014e9
No known key found for this signature in database
2 changed files with 139 additions and 51 deletions

View file

@ -5,8 +5,10 @@ import { useTheme } from "@/contexts/ThemeContext";
import { clearTokenCookies } from "@/utils/cookieUtils";
import { clearStoredReturnUrl } from "@/utils/returnUrlUtils";
import { fetchProxySettings } from "@/utils/proxyUtils";
import { MenuFoldOutlined, MenuUnfoldOutlined, MoonOutlined, SunOutlined } from "@ant-design/icons";
import { Button, Switch, Tag } from "antd";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Menu, Moon, PanelLeftClose, Sun } from "lucide-react";
import Link from "next/link";
import React, { useEffect, useState } from "react";
import { BlogDropdown } from "./Navbar/BlogDropdown/BlogDropdown";
@ -19,7 +21,9 @@ interface NavbarProps {
userEmail: string | null;
userRole: string | null;
premiumUser: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
proxySettings: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setProxySettings: React.Dispatch<React.SetStateAction<any>>;
accessToken: string | null;
isPublicPage: boolean;
@ -30,9 +34,13 @@ interface NavbarProps {
}
const Navbar: React.FC<NavbarProps> = ({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
userID,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
userEmail,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
userRole,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
premiumUser,
proxySettings,
setProxySettings,
@ -50,7 +58,6 @@ const Navbar: React.FC<NavbarProps> = ({
const version = healthData?.litellm_version;
const disableBouncingIcon = useDisableBouncingIcon();
// Simple logo URL: use custom logo if available, otherwise default
const imageUrl = logoUrl || `${baseUrl}/get_image`;
useEffect(() => {
@ -65,6 +72,7 @@ const Navbar: React.FC<NavbarProps> = ({
};
initializeProxySettings();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [accessToken]);
useEffect(() => {
@ -87,22 +95,32 @@ const Navbar: React.FC<NavbarProps> = ({
};
return (
<nav className="bg-white border-b border-gray-200 sticky top-0 z-10">
<nav className="bg-background border-b border-border sticky top-0 z-10">
<div className="w-full">
<div className="flex items-center h-14 px-4">
<div className="flex items-center flex-shrink-0">
{onToggleSidebar && (
<button
onClick={onToggleSidebar}
className="flex items-center justify-center w-10 h-10 mr-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded transition-colors"
className="flex items-center justify-center w-10 h-10 mr-2 text-muted-foreground hover:text-foreground hover:bg-muted rounded transition-colors"
title={sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
aria-label={
sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"
}
>
<span className="text-lg">{sidebarCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}</span>
{sidebarCollapsed ? (
<Menu className="h-5 w-5" />
) : (
<PanelLeftClose className="h-5 w-5" />
)}
</button>
)}
<div className="flex items-center gap-2">
<Link href={baseUrl ? baseUrl : "/"} className="flex items-center">
<Link
href={baseUrl ? baseUrl : "/"}
className="flex items-center"
>
<div className="relative">
<div className="h-10 max-w-48 flex items-center justify-center overflow-hidden">
<img
@ -124,7 +142,10 @@ const Navbar: React.FC<NavbarProps> = ({
🌑
</span>
)}
<Tag className="relative text-xs font-medium cursor-pointer z-10">
<Badge
variant="outline"
className="relative text-xs font-medium cursor-pointer z-10"
>
<a
href="https://docs.litellm.ai/release_notes"
target="_blank"
@ -133,28 +154,34 @@ const Navbar: React.FC<NavbarProps> = ({
>
v{version}
</a>
</Tag>
</Badge>
</div>
)}
</div>
</div>
{/* Right side nav items */}
<div className="flex items-center space-x-5 ml-auto">
<WorkerDropdown onWorkerSwitch={handleWorkerSwitch} />
<CommunityEngagementButtons />
{/* Dark mode is currently a work in progress. To test, you can change 'false' to 'true' below.
Do not set this to true by default until all components are confirmed to support dark mode styles. */}
{/* Dark mode is currently a work in progress. */}
{false && (
<Switch
data-testid="dark-mode-toggle"
checked={isDarkMode}
onChange={toggleDarkMode}
checkedChildren={<MoonOutlined />}
unCheckedChildren={<SunOutlined />}
/>
<div className="flex items-center gap-1">
<Sun className="h-3.5 w-3.5" />
<Switch
data-testid="dark-mode-toggle"
checked={isDarkMode}
onCheckedChange={toggleDarkMode}
/>
<Moon className="h-3.5 w-3.5" />
</div>
)}
<Button type="text" href="https://docs.litellm.ai/docs/" target="_blank" rel="noopener noreferrer">
Docs
<Button variant="ghost" asChild>
<a
href="https://docs.litellm.ai/docs/"
target="_blank"
rel="noopener noreferrer"
>
Docs
</a>
</Button>
<BlogDropdown />

View file

@ -1,7 +1,13 @@
import React, { useState, useEffect } from "react";
import { Card, Title, Text, TextInput, Button } from "@tremor/react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useTheme } from "@/contexts/ThemeContext";
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking";
import {
getProxyBaseUrl,
getGlobalLitellmHeaderName,
} from "@/components/networking";
import NotificationsManager from "./molecules/notifications_manager";
interface UIThemeSettingsProps {
@ -10,20 +16,29 @@ interface UIThemeSettingsProps {
accessToken: string | null;
}
const UIThemeSettings: React.FC<UIThemeSettingsProps> = ({ userID, userRole, accessToken }) => {
const { logoUrl, setLogoUrl, faviconUrl, setFaviconUrl } = useTheme();
const UIThemeSettings: React.FC<UIThemeSettingsProps> = ({
// eslint-disable-next-line @typescript-eslint/no-unused-vars
userID,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
userRole,
accessToken,
}) => {
const { setLogoUrl, setFaviconUrl } = useTheme();
const [logoUrlInput, setLogoUrlInput] = useState<string>("");
const [faviconUrlInput, setFaviconUrlInput] = useState<string>("");
const [loading, setLoading] = useState(false);
useEffect(() => {
if (accessToken) { fetchThemeSettings(); }
if (accessToken) fetchThemeSettings();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [accessToken]);
const fetchThemeSettings = async () => {
try {
const proxyBaseUrl = getProxyBaseUrl();
const url = proxyBaseUrl ? `${proxyBaseUrl}/get/ui_theme_settings` : "/get/ui_theme_settings";
const url = proxyBaseUrl
? `${proxyBaseUrl}/get/ui_theme_settings`
: "/get/ui_theme_settings";
const response = await fetch(url, {
method: "GET",
headers: {
@ -47,7 +62,9 @@ const UIThemeSettings: React.FC<UIThemeSettingsProps> = ({ userID, userRole, acc
setLoading(true);
try {
const proxyBaseUrl = getProxyBaseUrl();
const url = proxyBaseUrl ? `${proxyBaseUrl}/update/ui_theme_settings` : "/update/ui_theme_settings";
const url = proxyBaseUrl
? `${proxyBaseUrl}/update/ui_theme_settings`
: "/update/ui_theme_settings";
const response = await fetch(url, {
method: "PATCH",
headers: {
@ -63,20 +80,28 @@ const UIThemeSettings: React.FC<UIThemeSettingsProps> = ({ userID, userRole, acc
NotificationsManager.success("Theme settings updated successfully!");
setLogoUrl(logoUrlInput || null);
setFaviconUrl(faviconUrlInput || null);
} else { throw new Error("Failed to update settings"); }
} else {
throw new Error("Failed to update settings");
}
} catch (error) {
console.error("Error updating theme settings:", error);
NotificationsManager.fromBackend("Failed to update theme settings");
} finally { setLoading(false); }
} finally {
setLoading(false);
}
};
const handleReset = async () => {
setLogoUrlInput(""); setFaviconUrlInput("");
setLogoUrl(null); setFaviconUrl(null);
setLogoUrlInput("");
setFaviconUrlInput("");
setLogoUrl(null);
setFaviconUrl(null);
setLoading(true);
try {
const proxyBaseUrl = getProxyBaseUrl();
const url = proxyBaseUrl ? `${proxyBaseUrl}/update/ui_theme_settings` : "/update/ui_theme_settings";
const url = proxyBaseUrl
? `${proxyBaseUrl}/update/ui_theme_settings`
: "/update/ui_theme_settings";
const response = await fetch(url, {
method: "PATCH",
headers: {
@ -85,39 +110,75 @@ const UIThemeSettings: React.FC<UIThemeSettingsProps> = ({ userID, userRole, acc
},
body: JSON.stringify({ logo_url: null, favicon_url: null }),
});
if (response.ok) { NotificationsManager.success("Theme settings reset to default!"); }
else { throw new Error("Failed to reset"); }
if (response.ok) {
NotificationsManager.success("Theme settings reset to default!");
} else {
throw new Error("Failed to reset");
}
} catch (error) {
console.error("Error resetting theme settings:", error);
NotificationsManager.fromBackend("Failed to reset theme settings");
} finally { setLoading(false); }
} finally {
setLoading(false);
}
};
if (!accessToken) { return null; }
if (!accessToken) return null;
return (
<div className="w-full mx-auto max-w-4xl px-6 py-8">
<div className="mb-8">
<Title className="text-2xl font-bold mb-2">UI Theme Customization</Title>
<Text className="text-gray-600">Customize your LiteLLM admin dashboard with a custom logo and favicon.</Text>
<h1 className="text-2xl font-bold mb-2">UI Theme Customization</h1>
<p className="text-muted-foreground">
Customize your LiteLLM admin dashboard with a custom logo and favicon.
</p>
</div>
<Card className="shadow-sm p-6">
<div className="space-y-6">
<div>
<Text className="text-sm font-medium text-gray-700 mb-2 block">Custom Logo URL</Text>
<TextInput placeholder="https://example.com/logo.png" value={logoUrlInput}
onValueChange={(v) => { setLogoUrlInput(v); setLogoUrl(v || null); }} className="w-full" />
<Text className="text-xs text-gray-500 mt-1">Enter a URL for your custom logo or leave empty for default</Text>
<div className="space-y-2">
<Label htmlFor="ui-theme-logo">Custom Logo URL</Label>
<Input
id="ui-theme-logo"
placeholder="https://example.com/logo.png"
value={logoUrlInput}
onChange={(e) => {
const v = e.target.value;
setLogoUrlInput(v);
setLogoUrl(v || null);
}}
/>
<p className="text-xs text-muted-foreground">
Enter a URL for your custom logo or leave empty for default
</p>
</div>
<div>
<Text className="text-sm font-medium text-gray-700 mb-2 block">Custom Favicon URL</Text>
<TextInput placeholder="https://example.com/favicon.ico" value={faviconUrlInput}
onValueChange={(v) => { setFaviconUrlInput(v); setFaviconUrl(v || null); }} className="w-full" />
<Text className="text-xs text-gray-500 mt-1">Enter a URL for your custom favicon (.ico, .png, or .svg) or leave empty for default</Text>
<div className="space-y-2">
<Label htmlFor="ui-theme-favicon">Custom Favicon URL</Label>
<Input
id="ui-theme-favicon"
placeholder="https://example.com/favicon.ico"
value={faviconUrlInput}
onChange={(e) => {
const v = e.target.value;
setFaviconUrlInput(v);
setFaviconUrl(v || null);
}}
/>
<p className="text-xs text-muted-foreground">
Enter a URL for your custom favicon (.ico, .png, or .svg) or leave
empty for default
</p>
</div>
<div className="flex gap-3 pt-4">
<Button onClick={handleSave} loading={loading} disabled={loading} color="indigo">Save Changes</Button>
<Button onClick={handleReset} loading={loading} disabled={loading} variant="secondary" color="gray">Reset to Default</Button>
<Button onClick={handleSave} disabled={loading}>
{loading ? "Saving…" : "Save Changes"}
</Button>
<Button
variant="secondary"
onClick={handleReset}
disabled={loading}
>
Reset to Default
</Button>
</div>
</div>
</Card>