-
}
title="Budgets"
subtitle="Spend, TPM and RPM limits you can assign to customers."
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.tsx
index 91ad9f847f5..dc18a05edca 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.tsx
@@ -3,7 +3,7 @@ import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
import { Plus, SearchIcon, X } from "lucide-react";
import { parseAsString, useQueryState } from "nuqs";
import { useMemo, useState } from "react";
-import { PageHeader } from "@/components/shared/PageHeader";
+import { LegacyPageHeader } from "@/components/shared/LegacyPageHeader";
import { Button } from "@/components/ui/button";
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group";
import { CreateProjectModal } from "./ProjectModals/CreateProjectModal";
@@ -56,7 +56,7 @@ export function ProjectsPage() {
return (
-
{
expect(onUrlUpdate.mock.calls.at(-1)![0].searchParams.has("team")).toBe(false);
await waitFor(() => expect(screen.queryByTestId("team-info-view")).not.toBeInTheDocument());
});
+
+ it("should preserve the legacy inset for the team detail view", async () => {
+ renderWithQueryClient(, {
+ searchParams: "?team=team-from-url",
+ });
+
+ await waitFor(() => expect(mockTeamInfoView).toHaveBeenCalled());
+ expect(screen.getByRole("main")).toHaveClass("px-12", "py-6");
+ });
});
describe("Teams - Create Team CTA is grouped with the tabs on the left", () => {
@@ -512,21 +521,19 @@ describe("Teams - Create Team CTA is grouped with the tabs on the left", () => {
mockUseOrganizations.mockReturnValue({ data: [] });
});
- it("renders the Create Team button inside the tab bar, ahead of the tabs", () => {
+ it("should render the Create Team button inside the tab bar, ahead of the tabs", () => {
renderWithQueryClient();
- const createButton = screen.getByTestId("create-team-button");
- const tabBar = screen.getByRole("tablist").parentElement!;
+ const tabNav = screen.getByRole("tablist");
+ const createButton = within(tabNav).getByTestId("create-team-button");
+ const firstTab = within(tabNav).getByRole("tab", { name: "Your Teams" });
- // The CTA lives in the tab bar's left slot, not the standalone page header.
- expect(tabBar.contains(createButton)).toBe(true);
-
- // It reads as the left end of the cluster: it precedes the first tab in DOM order.
- const firstTab = screen.getByRole("tab", { name: "Your Teams" });
+ expect(screen.getByRole("main")).toHaveClass("p-8");
+ expect(within(tabNav).getByRole("separator")).toBeInTheDocument();
expect(createButton.compareDocumentPosition(firstTab) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
});
- it("omits the Create Team CTA for a role that cannot manage teams", () => {
+ it("should omit the Create Team CTA for a role that cannot manage teams", () => {
renderWithQueryClient();
expect(screen.queryByTestId("create-team-button")).not.toBeInTheDocument();
});
diff --git a/ui/litellm-dashboard/src/components/Teams.tsx b/ui/litellm-dashboard/src/components/Teams.tsx
index d515b67ad0f..8d9fdd7598f 100644
--- a/ui/litellm-dashboard/src/components/Teams.tsx
+++ b/ui/litellm-dashboard/src/components/Teams.tsx
@@ -15,7 +15,7 @@ import { SearchSelect } from "@/components/shared/SearchSelect";
import { labelWithDocsHint, labelWithHint } from "@/components/shared/form/LabelWithHint";
import { useZodForm } from "@/lib/forms/useZodForm";
import { TagsInput } from "@/app/(dashboard)/guardrails/_components/content_filter/TagsInput";
-import { Layout, Tabs, theme } from "antd";
+import { Layout, Tabs } from "antd";
import { ChevronDown, Plus, Users } from "lucide-react";
import React, { useEffect, useMemo, useState } from "react";
import { z } from "zod/v4";
@@ -542,7 +542,6 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser
return false;
};
- const { token } = theme.useToken();
const { Content } = Layout;
const tabItems = [
@@ -612,7 +611,7 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser
];
return (
-
+
{selectedTeamId ? (
= ({ accessToken, userID, userRole, premiumUser
premiumUser={premiumUser}
/>
) : (
- <>
-
-
}
- title="Teams"
- subtitle="Manage teams, members, and their access to models and budgets"
+
}
+ title="Teams"
+ subtitle="Manage teams, members, and their access to models and budgets"
+ primaryAction={
+ canCreateOrManageTeams(userRole, userID, organizations) ? (
+
setIsTeamModalVisible(true)} data-testid="create-team-button">
+
+ Create Team
+
+ ) : undefined
+ }
+ tabs={({ leadingControls }) => (
+
-
-
-
- setIsTeamModalVisible(true)} data-testid="create-team-button">
-
- Create Team
-
-
-
- ) : undefined,
- }}
- />
- >
+ )}
+ />
)}
{canCreateOrManageTeams(userRole, userID, organizations) && (
diff --git a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx
index fa0360c0dda..b278b4f675d 100644
--- a/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx
+++ b/ui/litellm-dashboard/src/components/VirtualKeysPage/VirtualKeysTable.tsx
@@ -12,7 +12,7 @@ import {
DataTableToolbar,
} from "@/components/shared/DataTable";
import { SearchSelect } from "@/components/shared/SearchSelect";
-import { PageHeader } from "@/components/shared/PageHeader";
+import { LegacyPageHeader } from "@/components/shared/LegacyPageHeader";
import { Input } from "@/components/ui/input";
import { useDebouncedValue } from "@tanstack/react-pacer/debouncer";
import { ColumnFiltersState, OnChangeFn, PaginationState, SortingState } from "@tanstack/react-table";
@@ -172,7 +172,7 @@ export function VirtualKeysTable({ headerActions }: VirtualKeysTableProps) {
return (
-
}
title="Virtual Keys"
subtitle="Every key that authenticates requests to the gateway."
diff --git a/ui/litellm-dashboard/src/components/shared/LegacyPageHeader.test.tsx b/ui/litellm-dashboard/src/components/shared/LegacyPageHeader.test.tsx
new file mode 100644
index 00000000000..a0081c1c7f1
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/shared/LegacyPageHeader.test.tsx
@@ -0,0 +1,33 @@
+import { renderWithProviders, screen } from "@/../tests/test-utils";
+import { describe, expect, it } from "vitest";
+
+import { LegacyPageHeader } from "./LegacyPageHeader";
+
+describe("LegacyPageHeader", () => {
+ it("should render the title as a heading", () => {
+ renderWithProviders(
);
+
+ expect(screen.getByRole("heading", { name: "Virtual Keys" })).toBeInTheDocument();
+ });
+
+ it("should render the optional identity and actions", () => {
+ renderWithProviders(
+
Key icon}
+ actions={}
+ />,
+ );
+
+ expect(screen.getByText("Every key that authenticates requests")).toBeInTheDocument();
+ expect(screen.getByText("Key icon")).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Create New Key" })).toBeInTheDocument();
+ });
+
+ it("should omit optional actions when none are provided", () => {
+ renderWithProviders();
+
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/shared/LegacyPageHeader.tsx b/ui/litellm-dashboard/src/components/shared/LegacyPageHeader.tsx
new file mode 100644
index 00000000000..43979ad00b4
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/shared/LegacyPageHeader.tsx
@@ -0,0 +1,25 @@
+"use client";
+
+import * as React from "react";
+
+interface LegacyPageHeaderProps {
+ title: React.ReactNode;
+ subtitle?: React.ReactNode;
+ icon?: React.ReactNode;
+ actions?: React.ReactNode;
+}
+
+export function LegacyPageHeader({ title, subtitle, icon, actions }: LegacyPageHeaderProps) {
+ return (
+
+
+ {icon != null &&
{icon}}
+
+
{title}
+ {subtitle != null &&
{subtitle}
}
+
+
+ {actions != null &&
{actions}
}
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/components/shared/PageHeader.test.tsx b/ui/litellm-dashboard/src/components/shared/PageHeader.test.tsx
index f7a313271da..3741542abad 100644
--- a/ui/litellm-dashboard/src/components/shared/PageHeader.test.tsx
+++ b/ui/litellm-dashboard/src/components/shared/PageHeader.test.tsx
@@ -1,31 +1,77 @@
-import { render, screen } from "@testing-library/react";
+import { renderWithProviders, screen, within } from "@/../tests/test-utils";
import { describe, expect, it } from "vitest";
import { PageHeader } from "./PageHeader";
+const identity = {
+ icon: Teams icon,
+ title: "Teams",
+ subtitle: "Manage teams, members, and their access to models and budgets",
+};
+
describe("PageHeader", () => {
- it("renders the title as a heading", () => {
- render();
- expect(screen.getByRole("heading", { name: "Virtual Keys" })).toBeInTheDocument();
+ it("should render the page identity", () => {
+ renderWithProviders();
+
+ expect(screen.getByRole("heading", { name: "Teams" })).toBeInTheDocument();
+ expect(screen.getByText("Teams icon").parentElement).toHaveAttribute("aria-hidden", "true");
+ expect(screen.getByText(identity.subtitle)).toBeInTheDocument();
});
- it("renders the subtitle, icon, and actions when provided", () => {
- render(
+ it("should apply the standard title and subtext typography", () => {
+ renderWithProviders();
+
+ const icon = screen.getByText("Teams icon").parentElement;
+ expect(screen.getByRole("heading", { name: "Teams" })).toHaveClass("text-2xl", "font-semibold", "tracking-tight");
+ expect(screen.getByText(identity.subtitle)).toHaveClass("mt-1.5", "text-sm", "text-muted-foreground");
+ expect(icon).toHaveClass("size-5", "[&_svg]:size-5", "[&_svg]:stroke-[1.75]");
+ expect(icon?.parentElement).toHaveClass("gap-2.5");
+ });
+
+ it("should render the primary action, divider, tabs, and utilities in the standard control row", () => {
+ renderWithProviders(
}
- actions={}
+ {...identity}
+ primaryAction={}
+ tabs={
+
+
+
+ }
+ utilities={}
/>,
);
- expect(screen.getByText("Every key that authenticates requests")).toBeInTheDocument();
- expect(screen.getByTestId("icon")).toBeInTheDocument();
- expect(screen.getByRole("button", { name: "Create New Key" })).toBeInTheDocument();
+
+ const controls = screen.getByRole("group", { name: "Page controls" });
+ expect(controls).toHaveClass("mt-5", "h-9");
+ expect(within(controls).getByRole("separator")).toHaveClass("mx-4", "h-6");
+ expect(controls).toHaveTextContent("Create TeamYour TeamsRefresh");
});
- it("omits the optional slots when not provided", () => {
- render();
- expect(screen.queryByRole("button")).not.toBeInTheDocument();
- expect(document.querySelector("p")).toBeNull();
+ it("should omit the divider when tabs are absent", () => {
+ renderWithProviders(Create Team} />);
+
+ expect(screen.queryByRole("separator")).not.toBeInTheDocument();
+ });
+
+ it("should provide standard controls to an embedded tab shell", () => {
+ renderWithProviders(
+ Create Team}
+ tabs={({ leadingControls, utilities }) => (
+
+ {leadingControls}
+
+ {utilities}
+
+ )}
+ utilities={}
+ />,
+ );
+
+ const tabs = screen.getByRole("tablist");
+ expect(within(tabs).getByRole("separator")).toBeInTheDocument();
+ expect(tabs).toHaveTextContent("Create TeamYour TeamsRefresh");
});
});
diff --git a/ui/litellm-dashboard/src/components/shared/PageHeader.tsx b/ui/litellm-dashboard/src/components/shared/PageHeader.tsx
index e314e8e8bc2..81092821efc 100644
--- a/ui/litellm-dashboard/src/components/shared/PageHeader.tsx
+++ b/ui/litellm-dashboard/src/components/shared/PageHeader.tsx
@@ -2,24 +2,57 @@
import * as React from "react";
-interface PageHeaderProps {
- title: React.ReactNode;
- subtitle?: React.ReactNode;
- icon?: React.ReactNode;
- actions?: React.ReactNode;
+import { ToolbarSeparator } from "./ToolbarSeparator";
+
+interface EmbeddedTabsSlots {
+ leadingControls: React.ReactNode;
+ utilities: React.ReactNode;
}
-export function PageHeader({ title, subtitle, icon, actions }: PageHeaderProps) {
- return (
-
-
- {icon != null &&
{icon}}
-
-
{title}
- {subtitle != null &&
{subtitle}
}
-
+interface PageHeaderProps {
+ title: React.ReactNode;
+ subtitle: React.ReactNode;
+ icon: React.ReactNode;
+ primaryAction?: React.ReactNode;
+ tabs?: React.ReactNode | ((slots: EmbeddedTabsSlots) => React.ReactNode);
+ utilities?: React.ReactNode;
+}
+
+export function PageHeader({ title, subtitle, icon, primaryAction, tabs, utilities }: PageHeaderProps) {
+ const leadingControls =
+ primaryAction == null ? null : (
+
+ {primaryAction}
+ {tabs != null && }
- {actions != null &&
{actions}
}
+ );
+ const utilityControls = utilities == null ? null :
{utilities}
;
+ const hasControlRow = primaryAction != null || tabs != null || utilities != null;
+
+ return (
+
+
+
+ {icon}
+
+
{title}
+
+
{subtitle}
+
+ {typeof tabs === "function" ? (
+
{tabs({ leadingControls, utilities: utilityControls })}
+ ) : (
+ hasControlRow && (
+
+ {leadingControls}
+ {tabs}
+ {utilityControls != null &&
{utilityControls}
}
+
+ )
+ )}
);
}