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(); });