Merge pull request #18547 from BerriAI/litellm_ui_turn_off_new_badges

[Feauture] UI - Hide New Badges
This commit is contained in:
yuneng-jiang 2025-12-31 11:19:07 -08:00 committed by GitHub
commit 0c0a5e5464
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 331 additions and 17 deletions

View file

@ -0,0 +1,35 @@
// hooks/useDisableShowNewBadge.ts
import { useSyncExternalStore } from "react";
import { getLocalStorageItem } from "@/utils/localStorageUtils";
import { LOCAL_STORAGE_EVENT } from "@/utils/localStorageUtils";
function subscribe(callback: () => void) {
const onStorage = (e: StorageEvent) => {
if (e.key === "disableShowNewBadge") {
callback();
}
};
const onCustom = (e: Event) => {
const { key } = (e as CustomEvent).detail;
if (key === "disableShowNewBadge") {
callback();
}
};
window.addEventListener("storage", onStorage);
window.addEventListener(LOCAL_STORAGE_EVENT, onCustom);
return () => {
window.removeEventListener("storage", onStorage);
window.removeEventListener(LOCAL_STORAGE_EVENT, onCustom);
};
}
function getSnapshot() {
return getLocalStorageItem("disableShowNewBadge") === "true";
}
export function useDisableShowNewBadge() {
return useSyncExternalStore(subscribe, getSnapshot);
}

View file

@ -27,7 +27,7 @@ import {
Text,
Title,
} from "@tremor/react";
import { Alert, Badge } from "antd";
import { Alert } from "antd";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useAgents } from "@/app/(dashboard)/hooks/agents/useAgents";
@ -419,13 +419,11 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
<div className="flex items-end justify-between gap-6 mb-6">
<div className="flex-1">
<div className="flex items-end justify-between gap-6 mb-4 w-full">
<Badge color="blue" count="New">
<UsageViewSelect
value={usageView}
onChange={(value) => setUsageView(value)}
isAdmin={all_admin_roles.includes(userRole || "")}
/>
</Badge>
<UsageViewSelect
value={usageView}
onChange={(value) => setUsageView(value)}
isAdmin={all_admin_roles.includes(userRole || "")}
/>
<AdvancedDatePicker value={dateValue} onValueChange={handleDateChange} />
</div>
{/* Your Usage Panel */}

View file

@ -0,0 +1,52 @@
import { render, screen } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import NewBadge from "./NewBadge";
// Mock the hook directly
vi.mock("@/app/(dashboard)/hooks/useDisableShowNewBadge", () => ({
useDisableShowNewBadge: vi.fn(),
}));
import { useDisableShowNewBadge } from "@/app/(dashboard)/hooks/useDisableShowNewBadge";
const mockUseDisableShowNewBadge = vi.mocked(useDisableShowNewBadge);
describe("NewBadge", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should render the badge when disableShowNewBadge is false", () => {
mockUseDisableShowNewBadge.mockReturnValue(false);
render(<NewBadge>Test Content</NewBadge>);
expect(screen.getByText("New")).toBeInTheDocument();
expect(screen.getByText("Test Content")).toBeInTheDocument();
});
it("should render the badge when disableShowNewBadge is not set", () => {
mockUseDisableShowNewBadge.mockReturnValue(false);
render(<NewBadge />);
expect(screen.getByText("New")).toBeInTheDocument();
});
it("should render only children when disableShowNewBadge is true", () => {
mockUseDisableShowNewBadge.mockReturnValue(true);
render(<NewBadge>Test Content</NewBadge>);
expect(screen.queryByText("New")).not.toBeInTheDocument();
expect(screen.getByText("Test Content")).toBeInTheDocument();
});
it("should render nothing when disableShowNewBadge is true and no children", () => {
mockUseDisableShowNewBadge.mockReturnValue(true);
const { container } = render(<NewBadge />);
expect(container.firstChild).toBeNull();
});
});

View file

@ -1,5 +1,18 @@
import { Badge } from "antd";
import { useDisableShowNewBadge } from "@/app/(dashboard)/hooks/useDisableShowNewBadge";
export default function NewBadge() {
return <Badge color="blue" count="New" />;
export default function NewBadge({ children }: { children?: React.ReactNode }) {
const disableShowNewBadge = useDisableShowNewBadge();
if (disableShowNewBadge) {
return children ? <>{children}</> : null;
}
return children ? (
<Badge color="blue" count="New">
{children}
</Badge>
) : (
<Badge color="blue" count="New" />
);
}

View file

@ -1,7 +1,7 @@
import Link from "next/link";
import React, { useState, useEffect } from "react";
import type { MenuProps } from "antd";
import { Dropdown, Tooltip } from "antd";
import { Dropdown, Switch, Tooltip } from "antd";
import { getProxyBaseUrl } from "@/components/networking";
import {
UserOutlined,
@ -13,8 +13,10 @@ import {
MenuUnfoldOutlined,
} from "@ant-design/icons";
import { clearTokenCookies } from "@/utils/cookieUtils";
import { getLocalStorageItem, setLocalStorageItem, removeLocalStorageItem } from "@/utils/localStorageUtils";
import { fetchProxySettings } from "@/utils/proxyUtils";
import { useTheme } from "@/contexts/ThemeContext";
import { emitLocalStorageChange } from "@/utils/localStorageUtils";
interface NavbarProps {
userID: string | null;
@ -44,6 +46,7 @@ const Navbar: React.FC<NavbarProps> = ({
const baseUrl = getProxyBaseUrl();
const [logoutUrl, setLogoutUrl] = useState("");
const [version, setVersion] = useState("");
const [disableShowNewBadge, setDisableShowNewBadge] = useState(false);
const { logoUrl } = useTheme();
// Simple logo URL: use custom logo if available, otherwise default
@ -79,6 +82,11 @@ const Navbar: React.FC<NavbarProps> = ({
initializeProxySettings();
}, [accessToken]);
useEffect(() => {
const storedValue = getLocalStorageItem("disableShowNewBadge");
setDisableShowNewBadge(storedValue === "true");
}, []);
useEffect(() => {
setLogoutUrl(proxySettings?.PROXY_LOGOUT_URL || "");
}, [proxySettings]);
@ -129,6 +137,28 @@ const Navbar: React.FC<NavbarProps> = ({
{userEmail || "Unknown"}
</span>
</div>
<div
className="flex items-center text-sm pt-2 mt-2 border-t border-gray-100"
onClick={(e) => e.stopPropagation()}
>
<span className="text-gray-500 text-xs">Hide New Feature Indicators</span>
<Switch
className="ml-auto"
size="small"
checked={disableShowNewBadge}
onChange={(checked) => {
setDisableShowNewBadge(checked);
if (checked) {
setLocalStorageItem("disableShowNewBadge", "true");
emitLocalStorageChange("disableShowNewBadge");
} else {
removeLocalStorageItem("disableShowNewBadge");
emitLocalStorageChange("disableShowNewBadge");
}
}}
aria-label="Toggle hide new feature indicators"
/>
</div>
</div>
</div>
),
@ -148,11 +178,7 @@ const Navbar: React.FC<NavbarProps> = ({
<nav className="bg-white border-b border-gray-200 sticky top-0 z-10">
<div className="w-full">
<div className="flex items-center h-14 px-4">
{" "}
{/* Increased height from h-12 to h-14 */}
{/* Left side with collapse toggle and logo */}
<div className="flex items-center flex-shrink-0">
{/* Collapse/Expand Toggle Button - Larger */}
{onToggleSidebar && (
<button
onClick={onToggleSidebar}
@ -167,9 +193,9 @@ const Navbar: React.FC<NavbarProps> = ({
<Link href="/" className="flex items-center">
<div className="relative">
<img src={imageUrl} alt="LiteLLM Brand" className="h-10 w-auto" />
<span
<span
className="absolute -top-1 -right-2 text-lg animate-bounce"
style={{ animationDuration: '2s' }}
style={{ animationDuration: "2s" }}
title="Happy Holidays!"
>
🎄

View file

@ -0,0 +1,157 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import {
emitLocalStorageChange,
LOCAL_STORAGE_EVENT,
getLocalStorageItem,
setLocalStorageItem,
removeLocalStorageItem,
} from "./localStorageUtils";
describe("emitLocalStorageChange", () => {
it("should dispatch a custom event with the provided key", () => {
const dispatchEventSpy = vi.spyOn(window, "dispatchEvent");
const testKey = "test-key";
emitLocalStorageChange(testKey);
expect(dispatchEventSpy).toHaveBeenCalledWith(expect.any(CustomEvent));
const dispatchedEvent = dispatchEventSpy.mock.calls[0][0] as CustomEvent;
expect(dispatchedEvent.type).toBe(LOCAL_STORAGE_EVENT);
expect(dispatchedEvent.detail).toEqual({ key: testKey });
dispatchEventSpy.mockRestore();
});
});
describe("getLocalStorageItem", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should return null when window is undefined", () => {
const originalWindow = global.window;
// @ts-ignore
delete global.window;
expect(getLocalStorageItem("test-key")).toBeNull();
global.window = originalWindow;
});
it("should return the stored value when it exists", () => {
const getItemSpy = vi.spyOn(Storage.prototype, "getItem").mockReturnValue("test-value");
const result = getLocalStorageItem("test-key");
expect(result).toBe("test-value");
expect(getItemSpy).toHaveBeenCalledWith("test-key");
getItemSpy.mockRestore();
});
it("should return null and log warning when localStorage throws an error", () => {
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const getItemSpy = vi.spyOn(Storage.prototype, "getItem").mockImplementation(() => {
throw new Error("Storage quota exceeded");
});
const result = getLocalStorageItem("test-key");
expect(result).toBeNull();
expect(consoleSpy).toHaveBeenCalledWith('Error reading localStorage key "test-key":', expect.any(Error));
consoleSpy.mockRestore();
getItemSpy.mockRestore();
});
});
describe("setLocalStorageItem", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should do nothing when window is undefined", () => {
const originalWindow = global.window;
// @ts-ignore
delete global.window;
const setItemSpy = vi.spyOn(Storage.prototype, "setItem");
setLocalStorageItem("test-key", "test-value");
expect(setItemSpy).not.toHaveBeenCalled();
global.window = originalWindow;
setItemSpy.mockRestore();
});
it("should set the item in localStorage", () => {
const setItemSpy = vi.spyOn(Storage.prototype, "setItem");
setLocalStorageItem("test-key", "test-value");
expect(setItemSpy).toHaveBeenCalledWith("test-key", "test-value");
setItemSpy.mockRestore();
});
it("should log warning when localStorage throws an error", () => {
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const setItemSpy = vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => {
throw new Error("Storage quota exceeded");
});
setLocalStorageItem("test-key", "test-value");
expect(consoleSpy).toHaveBeenCalledWith('Error setting localStorage key "test-key":', expect.any(Error));
consoleSpy.mockRestore();
setItemSpy.mockRestore();
});
});
describe("removeLocalStorageItem", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should do nothing when window is undefined", () => {
const originalWindow = global.window;
// @ts-ignore
delete global.window;
const removeItemSpy = vi.spyOn(Storage.prototype, "removeItem");
removeLocalStorageItem("test-key");
expect(removeItemSpy).not.toHaveBeenCalled();
global.window = originalWindow;
removeItemSpy.mockRestore();
});
it("should remove the item from localStorage", () => {
const removeItemSpy = vi.spyOn(Storage.prototype, "removeItem");
removeLocalStorageItem("test-key");
expect(removeItemSpy).toHaveBeenCalledWith("test-key");
removeItemSpy.mockRestore();
});
it("should log warning when localStorage throws an error", () => {
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const removeItemSpy = vi.spyOn(Storage.prototype, "removeItem").mockImplementation(() => {
throw new Error("Storage operation failed");
});
removeLocalStorageItem("test-key");
expect(consoleSpy).toHaveBeenCalledWith('Error removing localStorage key "test-key":', expect.any(Error));
consoleSpy.mockRestore();
removeItemSpy.mockRestore();
});
});

View file

@ -0,0 +1,33 @@
export const LOCAL_STORAGE_EVENT = "local-storage-change";
export function emitLocalStorageChange(key: string) {
window.dispatchEvent(new CustomEvent(LOCAL_STORAGE_EVENT, { detail: { key } }));
}
export function getLocalStorageItem(key: string): string | null {
if (typeof window === "undefined") return null;
try {
return window.localStorage.getItem(key);
} catch (error) {
console.warn(`Error reading localStorage key "${key}":`, error);
return null;
}
}
export function setLocalStorageItem(key: string, value: string): void {
if (typeof window === "undefined") return;
try {
window.localStorage.setItem(key, value);
} catch (error) {
console.warn(`Error setting localStorage key "${key}":`, error);
}
}
export function removeLocalStorageItem(key: string): void {
if (typeof window === "undefined") return;
try {
window.localStorage.removeItem(key);
} catch (error) {
console.warn(`Error removing localStorage key "${key}":`, error);
}
}