From ff4a40f0179426cb954b610f8d45d6a7bb268769 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:04:29 -0700 Subject: [PATCH] feat(ui): add reusable BetaBadge and use it for Projects sidebar item (#33449) * fix(ui): remove New badge from Projects sidebar item Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat(ui): add reusable BetaBadge and use it for Projects sidebar item Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * feat(ui): rename disableShowNewBadge flag to disableShowBadges and make BetaBadge respect it Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * style(ui): use blue for BetaBadge to match existing New badge Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * revert(ui): keep disableShowNewBadge localStorage key to preserve existing user opt-outs Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * chore: remove non-functional md artifacts from PR Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: ryan Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../src/components/BetaBadge.test.tsx | 70 +++++++++++++++++++ .../src/components/BetaBadge.tsx | 18 +++++ .../src/components/leftnav.tsx | 3 +- .../src/components/navbar.test.tsx | 2 +- 4 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/BetaBadge.test.tsx create mode 100644 ui/litellm-dashboard/src/components/BetaBadge.tsx diff --git a/ui/litellm-dashboard/src/components/BetaBadge.test.tsx b/ui/litellm-dashboard/src/components/BetaBadge.test.tsx new file mode 100644 index 00000000000..7eaf414087d --- /dev/null +++ b/ui/litellm-dashboard/src/components/BetaBadge.test.tsx @@ -0,0 +1,70 @@ +import { render, screen } from "@testing-library/react"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import BetaBadge from "./BetaBadge"; + +// 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("BetaBadge", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should render the badge when disableShowNewBadge is false", () => { + mockUseDisableShowNewBadge.mockReturnValue(false); + + render(Test Content); + + expect(screen.getByText("Beta")).toBeInTheDocument(); + expect(screen.getByText("Test Content")).toBeInTheDocument(); + }); + + it("should render the badge when disableShowNewBadge is not set", () => { + mockUseDisableShowNewBadge.mockReturnValue(false); + + render(); + + expect(screen.getByText("Beta")).toBeInTheDocument(); + }); + + it("should render only children when disableShowNewBadge is true", () => { + mockUseDisableShowNewBadge.mockReturnValue(true); + + render(Test Content); + + expect(screen.queryByText("Beta")).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(); + + expect(container.firstChild).toBeNull(); + }); + + it("should render badge with dot instead of text when dot prop is true", () => { + mockUseDisableShowNewBadge.mockReturnValue(false); + + render(Test Content); + + expect(screen.queryByText("Beta")).not.toBeInTheDocument(); + expect(screen.getByText("Test Content")).toBeInTheDocument(); + }); + + it("should render badge with 'Beta' text when dot prop is not provided (defaults to false)", () => { + mockUseDisableShowNewBadge.mockReturnValue(false); + + render(Test Content); + + expect(screen.getByText("Beta")).toBeInTheDocument(); + expect(screen.getByText("Test Content")).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/BetaBadge.tsx b/ui/litellm-dashboard/src/components/BetaBadge.tsx new file mode 100644 index 00000000000..7c4ef04417e --- /dev/null +++ b/ui/litellm-dashboard/src/components/BetaBadge.tsx @@ -0,0 +1,18 @@ +import { Badge } from "antd"; +import { useDisableShowNewBadge } from "@/app/(dashboard)/hooks/useDisableShowNewBadge"; + +export default function BetaBadge({ children, dot = false }: { children?: React.ReactNode; dot?: boolean }) { + const disableShowNewBadge = useDisableShowNewBadge(); + + if (disableShowNewBadge) { + return children ? <>{children} : null; + } + + return children ? ( + + {children} + + ) : ( + + ); +} diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index bf3440e7ab9..a2e2baf374e 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -72,6 +72,7 @@ import { rolesAllowedToViewWriteScopedPages, rolesWithWriteAccess, } from "../utils/roles"; +import BetaBadge from "./BetaBadge"; import NewBadge from "./common_components/NewBadge"; import type { Organization } from "./networking"; import SidebarAccountMenu from "./SidebarAccountMenu/SidebarAccountMenu"; @@ -211,7 +212,7 @@ const menuGroups: MenuGroup[] = [ page: "projects", label: ( - Projects + Projects ), icon: , diff --git a/ui/litellm-dashboard/src/components/navbar.test.tsx b/ui/litellm-dashboard/src/components/navbar.test.tsx index bdd0681dfa8..ba0b4bf55f5 100644 --- a/ui/litellm-dashboard/src/components/navbar.test.tsx +++ b/ui/litellm-dashboard/src/components/navbar.test.tsx @@ -252,7 +252,7 @@ describe("Navbar", () => { expect(screen.queryByRole("button", { name: /^notifications$/i })).not.toBeInTheDocument(); }); - it("should handle hide new features toggle", async () => { + it("should handle hide new feature indicators toggle", async () => { const user = userEvent.setup(); // Initially disabled