From 619d67d05c9bd77a1c9c522384353d6f4caafc1c Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Tue, 15 Sep 2026 23:13:26 +0000
Subject: [PATCH 1/3] fix(ui): show Projects nav to team and org admins
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---
.../src/components/leftnav.test.tsx | 121 +++++++++++++++++-
.../src/components/leftnav.tsx | 8 +-
2 files changed, 124 insertions(+), 5 deletions(-)
diff --git a/ui/litellm-dashboard/src/components/leftnav.test.tsx b/ui/litellm-dashboard/src/components/leftnav.test.tsx
index 6eb0218c41d..555aa03fbe0 100644
--- a/ui/litellm-dashboard/src/components/leftnav.test.tsx
+++ b/ui/litellm-dashboard/src/components/leftnav.test.tsx
@@ -13,7 +13,7 @@ vi.mock("../utils/roles", async (importOriginal) => {
rolesWithWriteAccess: ["admin", "internal"],
rolesAllowedToViewWriteScopedPages: ["admin", "internal", "admin_viewer"],
isAdminRole: (role: string) => role === "admin" || role === "admin_viewer",
- isUserTeamAdminForAnyTeam: () => false,
+ isUserTeamAdminForAnyTeam: actual.isUserTeamAdminForAnyTeam,
};
});
@@ -23,7 +23,7 @@ vi.mock("next/navigation", () => ({
usePathname: () => navState.pathname,
}));
-const { mockUseAuthorized, mockUseOrganizations } = vi.hoisted(() => {
+const { mockUseAuthorized, mockUseOrganizations, mockUseTeams } = vi.hoisted(() => {
const mockUseAuthorized = vi.fn(() => ({
userId: "test-user-id",
accessToken: "test-access-token",
@@ -42,7 +42,13 @@ const { mockUseAuthorized, mockUseOrganizations } = vi.hoisted(() => {
error: null,
}));
- return { mockUseAuthorized, mockUseOrganizations };
+ const mockUseTeams = vi.fn(() => ({
+ data: [] as Array<{ members_with_roles: Array<{ user_id: string; role: string }> }>,
+ isLoading: false,
+ error: null,
+ }));
+
+ return { mockUseAuthorized, mockUseOrganizations, mockUseTeams };
});
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
@@ -54,7 +60,7 @@ vi.mock("@/app/(dashboard)/hooks/organizations/useOrganizations", () => ({
}));
vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({
- useTeams: () => ({ data: [], isLoading: false, error: null }),
+ useTeams: mockUseTeams,
}));
vi.mock("@/app/(dashboard)/hooks/uiConfig/useUIConfig", () => {
@@ -110,6 +116,12 @@ describe("Sidebar (leftnav)", () => {
afterEach(() => {
mockUseAuthorized.mockReset();
mockUseOrganizations.mockReset();
+ mockUseTeams.mockReset();
+ mockUseTeams.mockReturnValue({
+ data: [],
+ isLoading: false,
+ error: null,
+ });
mockUseThemeImpl = unbrandedTheme;
navState.pathname = "/ui/api-keys";
});
@@ -514,6 +526,107 @@ describe("Sidebar (leftnav)", () => {
expect(screen.getByText("Organizations")).toBeInTheDocument();
});
+ it("shows Projects to an internal user who administers a team", () => {
+ mockUseAuthorized.mockReturnValue({
+ userId: "team-admin-user-id",
+ accessToken: "test-access-token",
+ userRole: "internal",
+ isViewOnly: false,
+ token: "test-token",
+ userEmail: "teamadmin@example.com",
+ premiumUser: false,
+ disabledPersonalKeyCreation: false,
+ showSSOBanner: false,
+ });
+ mockUseTeams.mockReturnValue({
+ data: [
+ {
+ members_with_roles: [{ user_id: "team-admin-user-id", role: "admin" }],
+ },
+ ],
+ isLoading: false,
+ error: null,
+ });
+
+ renderWithProviders();
+
+ expect(screen.getByRole("link", { name: /Projects/ })).toBeInTheDocument();
+ });
+
+ it("hides Projects when the feature flag is disabled for a team admin", () => {
+ mockUseAuthorized.mockReturnValue({
+ userId: "team-admin-user-id",
+ accessToken: "test-access-token",
+ userRole: "internal",
+ isViewOnly: false,
+ token: "test-token",
+ userEmail: "teamadmin@example.com",
+ premiumUser: false,
+ disabledPersonalKeyCreation: false,
+ showSSOBanner: false,
+ });
+ mockUseTeams.mockReturnValue({
+ data: [
+ {
+ members_with_roles: [{ user_id: "team-admin-user-id", role: "admin" }],
+ },
+ ],
+ isLoading: false,
+ error: null,
+ });
+
+ renderWithProviders();
+
+ expect(screen.queryByRole("link", { name: /Projects/ })).not.toBeInTheDocument();
+ });
+
+ it("hides Projects from an internal user who is not a team admin", () => {
+ mockUseAuthorized.mockReturnValue({
+ userId: "team-member-user-id",
+ accessToken: "test-access-token",
+ userRole: "internal",
+ isViewOnly: false,
+ token: "test-token",
+ userEmail: "teamuser@example.com",
+ premiumUser: false,
+ disabledPersonalKeyCreation: false,
+ showSSOBanner: false,
+ });
+
+ renderWithProviders();
+
+ expect(screen.queryByRole("link", { name: /Projects/ })).not.toBeInTheDocument();
+ });
+
+ it("applies the internal-user page allowlist to Projects for team admins", () => {
+ mockUseAuthorized.mockReturnValue({
+ userId: "team-admin-user-id",
+ accessToken: "test-access-token",
+ userRole: "internal",
+ isViewOnly: false,
+ token: "test-token",
+ userEmail: "teamadmin@example.com",
+ premiumUser: false,
+ disabledPersonalKeyCreation: false,
+ showSSOBanner: false,
+ });
+ mockUseTeams.mockReturnValue({
+ data: [
+ {
+ members_with_roles: [{ user_id: "team-admin-user-id", role: "admin" }],
+ },
+ ],
+ isLoading: false,
+ error: null,
+ });
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.queryByRole("link", { name: /Projects/ })).not.toBeInTheDocument();
+ });
+
it("marks the nav item for the current route active", () => {
navState.pathname = "/ui/logs";
renderWithProviders();
diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx
index 9d772f45153..e0b43c9aad9 100644
--- a/ui/litellm-dashboard/src/components/leftnav.tsx
+++ b/ui/litellm-dashboard/src/components/leftnav.tsx
@@ -475,7 +475,13 @@ const Sidebar_: React.FC = ({
if (!isAdmin && enabledPagesInternalUsers != null) return enabledPagesInternalUsers.includes(item.page);
return true;
}
- if (item.key === "projects" && !enableProjectsUI) return false;
+ if (item.key === "projects") {
+ if (!enableProjectsUI) return false;
+ const hasRoleAccess = isAdmin || isOrgAdmin || isTeamAdmin;
+ if (!hasRoleAccess) return false;
+ if (!isAdmin && enabledPagesInternalUsers != null) return enabledPagesInternalUsers.includes(item.page);
+ return true;
+ }
if (
!isAdmin &&
item.key === "agents" &&
From f35df471a8d5bfaa7c8bb7ffadbfc38f163947ee Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Tue, 15 Sep 2026 23:19:00 +0000
Subject: [PATCH 2/3] fix(ui): satisfy frontend lint budget
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---
.../src/components/leftnav.test.tsx | 72 ++++++++-----------
1 file changed, 28 insertions(+), 44 deletions(-)
diff --git a/ui/litellm-dashboard/src/components/leftnav.test.tsx b/ui/litellm-dashboard/src/components/leftnav.test.tsx
index 555aa03fbe0..be3c5fe878e 100644
--- a/ui/litellm-dashboard/src/components/leftnav.test.tsx
+++ b/ui/litellm-dashboard/src/components/leftnav.test.tsx
@@ -108,6 +108,30 @@ const placementsOf = (page: string): string[] =>
),
]);
+const teamAdminAuthorization = {
+ userId: "team-admin-user-id",
+ accessToken: "test-access-token",
+ userRole: "internal",
+ isViewOnly: false,
+ token: "test-token",
+ userEmail: "teamadmin@example.com",
+ premiumUser: false,
+ disabledPersonalKeyCreation: false,
+ showSSOBanner: false,
+};
+
+const teamMemberAuthorization = {
+ userId: "team-member-user-id",
+ accessToken: "test-access-token",
+ userRole: "internal",
+ isViewOnly: false,
+ token: "test-token",
+ userEmail: "teamuser@example.com",
+ premiumUser: false,
+ disabledPersonalKeyCreation: false,
+ showSSOBanner: false,
+};
+
describe("Sidebar (leftnav)", () => {
const defaultProps = {
collapsed: false,
@@ -527,17 +551,7 @@ describe("Sidebar (leftnav)", () => {
});
it("shows Projects to an internal user who administers a team", () => {
- mockUseAuthorized.mockReturnValue({
- userId: "team-admin-user-id",
- accessToken: "test-access-token",
- userRole: "internal",
- isViewOnly: false,
- token: "test-token",
- userEmail: "teamadmin@example.com",
- premiumUser: false,
- disabledPersonalKeyCreation: false,
- showSSOBanner: false,
- });
+ mockUseAuthorized.mockReturnValue(teamAdminAuthorization);
mockUseTeams.mockReturnValue({
data: [
{
@@ -554,17 +568,7 @@ describe("Sidebar (leftnav)", () => {
});
it("hides Projects when the feature flag is disabled for a team admin", () => {
- mockUseAuthorized.mockReturnValue({
- userId: "team-admin-user-id",
- accessToken: "test-access-token",
- userRole: "internal",
- isViewOnly: false,
- token: "test-token",
- userEmail: "teamadmin@example.com",
- premiumUser: false,
- disabledPersonalKeyCreation: false,
- showSSOBanner: false,
- });
+ mockUseAuthorized.mockReturnValue(teamAdminAuthorization);
mockUseTeams.mockReturnValue({
data: [
{
@@ -581,17 +585,7 @@ describe("Sidebar (leftnav)", () => {
});
it("hides Projects from an internal user who is not a team admin", () => {
- mockUseAuthorized.mockReturnValue({
- userId: "team-member-user-id",
- accessToken: "test-access-token",
- userRole: "internal",
- isViewOnly: false,
- token: "test-token",
- userEmail: "teamuser@example.com",
- premiumUser: false,
- disabledPersonalKeyCreation: false,
- showSSOBanner: false,
- });
+ mockUseAuthorized.mockReturnValue(teamMemberAuthorization);
renderWithProviders();
@@ -599,17 +593,7 @@ describe("Sidebar (leftnav)", () => {
});
it("applies the internal-user page allowlist to Projects for team admins", () => {
- mockUseAuthorized.mockReturnValue({
- userId: "team-admin-user-id",
- accessToken: "test-access-token",
- userRole: "internal",
- isViewOnly: false,
- token: "test-token",
- userEmail: "teamadmin@example.com",
- premiumUser: false,
- disabledPersonalKeyCreation: false,
- showSSOBanner: false,
- });
+ mockUseAuthorized.mockReturnValue(teamAdminAuthorization);
mockUseTeams.mockReturnValue({
data: [
{
From 821cbe9c85734d486478c671280e0ee89d10e13c Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Tue, 15 Sep 2026 23:24:45 +0000
Subject: [PATCH 3/3] style(ui): format leftnav regression tests
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---
ui/litellm-dashboard/src/components/leftnav.test.tsx | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/ui/litellm-dashboard/src/components/leftnav.test.tsx b/ui/litellm-dashboard/src/components/leftnav.test.tsx
index be3c5fe878e..a292f3b1af0 100644
--- a/ui/litellm-dashboard/src/components/leftnav.test.tsx
+++ b/ui/litellm-dashboard/src/components/leftnav.test.tsx
@@ -604,9 +604,7 @@ describe("Sidebar (leftnav)", () => {
error: null,
});
- renderWithProviders(
- ,
- );
+ renderWithProviders();
expect(screen.queryByRole("link", { name: /Projects/ })).not.toBeInTheDocument();
});