mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
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:
parent
32392518f6
commit
1c27c014e9
2 changed files with 139 additions and 51 deletions
|
|
@ -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 />
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue