mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
Merge pull request #18547 from BerriAI/litellm_ui_turn_off_new_badges
[Feauture] UI - Hide New Badges
This commit is contained in:
commit
0c0a5e5464
7 changed files with 331 additions and 17 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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" />
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!"
|
||||
>
|
||||
🎄
|
||||
|
|
|
|||
157
ui/litellm-dashboard/src/utils/localStorageUtils.test.ts
Normal file
157
ui/litellm-dashboard/src/utils/localStorageUtils.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
33
ui/litellm-dashboard/src/utils/localStorageUtils.ts
Normal file
33
ui/litellm-dashboard/src/utils/localStorageUtils.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue