From f05d02aad23280c4f10d0e855ad019612222512c Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Fri, 12 Jun 2026 21:35:37 -0700 Subject: [PATCH] feat(ui): cut the teams page over to the /ui/teams path route OldTeams renders from app/(dashboard)/teams/page.tsx via useAuthorized. The component already fetched its own paginated, filtered team list through v2TeamListCall; setTeams was only a round-trip back into the shell's lifted state, so it becomes internal useState. The organizations prop was redundant with the useOrganizations hook the component already calls, and searchParams was never read, so both are dropped. The shell keeps its own teams state and fetch for the still-coupled api-keys and models arms. Tests no longer inject teams through a prop; they mock teamListCall to drive what the component renders. --- .../e2e_tests/fixtures/migratedPages.ts | 1 + .../src/app/(dashboard)/page.tsx | 12 - .../src/app/(dashboard)/teams/page.tsx | 9 + .../src/components/OldTeams.test.tsx | 640 ++++++++---------- .../src/components/OldTeams.tsx | 22 +- .../src/utils/migratedPages.test.ts | 7 + .../src/utils/migratedPages.ts | 1 + 7 files changed, 309 insertions(+), 383 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/teams/page.tsx diff --git a/ui/litellm-dashboard/e2e_tests/fixtures/migratedPages.ts b/ui/litellm-dashboard/e2e_tests/fixtures/migratedPages.ts index a911bbb17c4..04bf393933d 100644 --- a/ui/litellm-dashboard/e2e_tests/fixtures/migratedPages.ts +++ b/ui/litellm-dashboard/e2e_tests/fixtures/migratedPages.ts @@ -40,6 +40,7 @@ export const MIGRATED_E2E_PAGES: Record = { agents: "agents", "router-settings": "router-settings", users: "users", + teams: "teams", }; export const MIGRATED_E2E_SEGMENTS: string[] = [...new Set(Object.values(MIGRATED_E2E_PAGES))]; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/page.tsx index 2ef7839dadc..7fff153c370 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/page.tsx @@ -6,7 +6,6 @@ import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings" import LoadingScreen from "@/components/common_components/LoadingScreen"; import { Team } from "@/components/key_team_helpers/key_list"; import { Organization, proxyBaseUrl, getInProductNudgesCall } from "@/components/networking"; -import OldTeams from "@/components/OldTeams"; import { fetchUserModels, CreateKeyPrefillData } from "@/components/organisms/create_key_button"; import Organizations, { fetchOrganizations } from "@/components/organizations"; import PassThroughSettings from "@/components/pass_through_settings"; @@ -322,17 +321,6 @@ function CreateKeyPageContent() { premiumUser={premiumUser} teams={teams} /> - ) : page == "teams" ? ( - ) : page == "organizations" ? ( ; +} diff --git a/ui/litellm-dashboard/src/components/OldTeams.test.tsx b/ui/litellm-dashboard/src/components/OldTeams.test.tsx index 7447432b876..530df8b3701 100644 --- a/ui/litellm-dashboard/src/components/OldTeams.test.tsx +++ b/ui/litellm-dashboard/src/components/OldTeams.test.tsx @@ -5,6 +5,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { fetchAvailableModelsForTeamOrKey } from "./key_team_helpers/fetch_available_models_team_key"; import { fetchMCPAccessGroups, getGuardrailsList, teamCreateCall } from "./networking"; import OldTeams from "./OldTeams"; +import { teamListCall } from "@/app/(dashboard)/hooks/teams/useTeams"; const mockTeamInfoView = vi.fn(); const mockUseOrganizations = vi.fn(); @@ -349,32 +350,29 @@ describe("OldTeams - handleCreate organization handling", () => { it("should clear the delete modal when the cancel button is clicked", async () => { mockUseOrganizations.mockReturnValue({ data: [] }); - renderWithQueryClient( - , - ); + vi.mocked(teamListCall).mockResolvedValue({ + teams: [ + { + team_id: "1", + team_alias: "Test Team", + organization_id: "org-123", + models: ["gpt-4"], + max_budget: 100, + budget_duration: "1d", + tpm_limit: 1000, + rpm_limit: 1000, + created_at: new Date().toISOString(), + keys: [], + members_with_roles: [], + spend: 0, + }, + ], + total: 1, + page: 1, + page_size: 100, + total_pages: 1, + } as any); + renderWithQueryClient(); await waitFor(() => { expect(screen.getByTestId("delete-team-button")).toBeInTheDocument(); }); @@ -393,17 +391,8 @@ describe("OldTeams - empty state", () => { }); it("should display empty state message when teams array is empty", async () => { - renderWithQueryClient( - , - ); + vi.mocked(teamListCall).mockResolvedValue({ teams: [], total: 0, page: 1, page_size: 100, total_pages: 1 } as any); + renderWithQueryClient(); await waitFor(() => { expect(screen.getByText("No teams yet")).toBeInTheDocument(); @@ -414,17 +403,8 @@ describe("OldTeams - empty state", () => { }); it("should display empty state message when teams is null", async () => { - renderWithQueryClient( - , - ); + vi.mocked(teamListCall).mockResolvedValue({ teams: [], total: 0, page: 1, page_size: 100, total_pages: 1 } as any); + renderWithQueryClient(); await waitFor(() => { expect(screen.getByText("No teams yet")).toBeInTheDocument(); @@ -435,32 +415,29 @@ describe("OldTeams - empty state", () => { }); it("should not display empty state when teams array has items", async () => { - renderWithQueryClient( - , - ); + vi.mocked(teamListCall).mockResolvedValue({ + teams: [ + { + team_id: "1", + team_alias: "Test Team", + organization_id: "org-123", + models: ["gpt-4"], + max_budget: 100, + budget_duration: "1d", + tpm_limit: 1000, + rpm_limit: 1000, + created_at: new Date().toISOString(), + keys: [], + members_with_roles: [], + spend: 0, + }, + ], + total: 1, + page: 1, + page_size: 100, + total_pages: 1, + } as any); + renderWithQueryClient(); await waitFor(() => { expect(screen.getByText("Test Team")).toBeInTheDocument(); @@ -608,33 +585,29 @@ describe("OldTeams - premium props", () => { }); it("passes premiumUser flag to TeamInfoView", async () => { - renderWithQueryClient( - , - ); + vi.mocked(teamListCall).mockResolvedValue({ + teams: [ + { + team_id: "team-123456789", + team_alias: "Premium Team", + organization_id: "org-123", + models: ["gpt-4"], + max_budget: 100, + budget_duration: "1d", + tpm_limit: 1000, + rpm_limit: 1000, + created_at: new Date().toISOString(), + keys: [], + members_with_roles: [], + spend: 0, + }, + ], + total: 1, + page: 1, + page_size: 100, + total_pages: 1, + } as any); + renderWithQueryClient(); const teamIdElement = await screen.findByText("team-123456789"); act(() => { @@ -654,125 +627,113 @@ describe("OldTeams - Default Team Settings tab visibility", () => { }); it("should show Default Team Settings tab for Admin role", () => { - renderWithQueryClient( - , - ); + vi.mocked(teamListCall).mockResolvedValue({ + teams: [ + { + team_id: "1", + team_alias: "Test Team", + organization_id: "org-123", + models: ["gpt-4"], + max_budget: 100, + budget_duration: "1d", + tpm_limit: 1000, + rpm_limit: 1000, + created_at: new Date().toISOString(), + keys: [], + members_with_roles: [], + spend: 0, + }, + ], + total: 1, + page: 1, + page_size: 100, + total_pages: 1, + } as any); + renderWithQueryClient(); expect(screen.getByRole("tab", { name: "Default Team Settings" })).toBeInTheDocument(); }); it("should show Default Team Settings tab for proxy_admin role", () => { - renderWithQueryClient( - , - ); + vi.mocked(teamListCall).mockResolvedValue({ + teams: [ + { + team_id: "1", + team_alias: "Test Team", + organization_id: "org-123", + models: ["gpt-4"], + max_budget: 100, + budget_duration: "1d", + tpm_limit: 1000, + rpm_limit: 1000, + created_at: new Date().toISOString(), + keys: [], + members_with_roles: [], + spend: 0, + }, + ], + total: 1, + page: 1, + page_size: 100, + total_pages: 1, + } as any); + renderWithQueryClient(); expect(screen.getByRole("tab", { name: "Default Team Settings" })).toBeInTheDocument(); }); it("should not show Default Team Settings tab for proxy_admin_viewer role", () => { - renderWithQueryClient( - , - ); + vi.mocked(teamListCall).mockResolvedValue({ + teams: [ + { + team_id: "1", + team_alias: "Test Team", + organization_id: "org-123", + models: ["gpt-4"], + max_budget: 100, + budget_duration: "1d", + tpm_limit: 1000, + rpm_limit: 1000, + created_at: new Date().toISOString(), + keys: [], + members_with_roles: [], + spend: 0, + }, + ], + total: 1, + page: 1, + page_size: 100, + total_pages: 1, + } as any); + renderWithQueryClient(); expect(screen.queryByRole("tab", { name: "Default Team Settings" })).not.toBeInTheDocument(); }); it("should not show Default Team Settings tab for Admin Viewer role", () => { - renderWithQueryClient( - , - ); + vi.mocked(teamListCall).mockResolvedValue({ + teams: [ + { + team_id: "1", + team_alias: "Test Team", + organization_id: "org-123", + models: ["gpt-4"], + max_budget: 100, + budget_duration: "1d", + tpm_limit: 1000, + rpm_limit: 1000, + created_at: new Date().toISOString(), + keys: [], + members_with_roles: [], + spend: 0, + }, + ], + total: 1, + page: 1, + page_size: 100, + total_pages: 1, + } as any); + renderWithQueryClient(); expect(screen.queryByRole("tab", { name: "Default Team Settings" })).not.toBeInTheDocument(); }); @@ -800,17 +761,8 @@ describe("OldTeams - access_group_ids in team create", () => { }); it("should pass access_group_ids to teamCreateCall when creating team", async () => { - renderWithQueryClient( - , - ); + vi.mocked(teamListCall).mockResolvedValue({ teams: [], total: 0, page: 1, page_size: 100, total_pages: 1 } as any); + renderWithQueryClient(); const createButton = screen.getAllByRole("button", { name: /create team/i })[0]; act(() => { @@ -864,17 +816,8 @@ describe("OldTeams - models dropdown options", () => { it("should not render all-proxy-models option in models select", async () => { vi.mocked(fetchAvailableModelsForTeamOrKey).mockResolvedValue(["gpt-4", "gpt-3.5-turbo"]); - renderWithQueryClient( - , - ); + vi.mocked(teamListCall).mockResolvedValue({ teams: [], total: 0, page: 1, page_size: 100, total_pages: 1 } as any); + renderWithQueryClient(); await waitFor(() => { expect(fetchAvailableModelsForTeamOrKey).toHaveBeenCalled(); @@ -922,32 +865,29 @@ describe("OldTeams - organization alias display", () => { mockUseOrganizations.mockReturnValue({ data: mockOrganizations }); - renderWithQueryClient( - , - ); + vi.mocked(teamListCall).mockResolvedValue({ + teams: [ + { + team_id: "1", + team_alias: "Test Team", + organization_id: "org-123", + models: ["gpt-4"], + max_budget: 100, + budget_duration: "1d", + tpm_limit: 1000, + rpm_limit: 1000, + created_at: new Date().toISOString(), + keys: [], + members_with_roles: [], + spend: 0, + }, + ], + total: 1, + page: 1, + page_size: 100, + total_pages: 1, + } as any); + renderWithQueryClient(); await waitFor(() => { expect(screen.getByText("Test Organization")).toBeInTheDocument(); @@ -958,32 +898,29 @@ describe("OldTeams - organization alias display", () => { it("should display organization id when alias is not found", async () => { mockUseOrganizations.mockReturnValue({ data: [] }); - renderWithQueryClient( - , - ); + vi.mocked(teamListCall).mockResolvedValue({ + teams: [ + { + team_id: "1", + team_alias: "Test Team", + organization_id: "org-unknown", + models: ["gpt-4"], + max_budget: 100, + budget_duration: "1d", + tpm_limit: 1000, + rpm_limit: 1000, + created_at: new Date().toISOString(), + keys: [], + members_with_roles: [], + spend: 0, + }, + ], + total: 1, + page: 1, + page_size: 100, + total_pages: 1, + } as any); + renderWithQueryClient(); await waitFor(() => { expect(screen.getByText("org-unknown")).toBeInTheDocument(); @@ -993,32 +930,29 @@ describe("OldTeams - organization alias display", () => { it("should display N/A when organization_id is null", async () => { mockUseOrganizations.mockReturnValue({ data: [] }); - renderWithQueryClient( - , - ); + vi.mocked(teamListCall).mockResolvedValue({ + teams: [ + { + team_id: "1", + team_alias: "Test Team", + organization_id: null as any, + models: ["gpt-4"], + max_budget: 100, + budget_duration: "1d", + tpm_limit: 1000, + rpm_limit: 1000, + created_at: new Date().toISOString(), + keys: [], + members_with_roles: [], + spend: 0, + }, + ], + total: 1, + page: 1, + page_size: 100, + total_pages: 1, + } as any); + renderWithQueryClient(); await waitFor(() => { // When organization_id is null, the table shows "—" in the Organization column @@ -1034,32 +968,31 @@ describe("OldTeams - Resources column keys badge", () => { }); it("renders keys_count from the v2 payload in the Resources badge", async () => { + vi.mocked(teamListCall).mockResolvedValue({ + teams: [ + { + team_id: "1", + team_alias: "Team With Keys", + organization_id: "org-123", + models: ["gpt-4"], + max_budget: 100, + budget_duration: "1d", + tpm_limit: 1000, + rpm_limit: 1000, + created_at: new Date().toISOString(), + keys: [], + keys_count: 3, + members_with_roles: [], + spend: 0, + } as any, + ], + total: 1, + page: 1, + page_size: 100, + total_pages: 1, + } as any); const { container } = renderWithQueryClient( - , + , ); await waitFor(() => { @@ -1071,31 +1004,30 @@ describe("OldTeams - Resources column keys badge", () => { }); it("falls back to keys.length when keys_count is absent", async () => { + vi.mocked(teamListCall).mockResolvedValue({ + teams: [ + { + team_id: "2", + team_alias: "Legacy Team", + organization_id: "org-123", + models: ["gpt-4"], + max_budget: 100, + budget_duration: "1d", + tpm_limit: 1000, + rpm_limit: 1000, + created_at: new Date().toISOString(), + keys: [{ token: "t1" } as any, { token: "t2" } as any], + members_with_roles: [], + spend: 0, + } as any, + ], + total: 1, + page: 1, + page_size: 100, + total_pages: 1, + } as any); const { container } = renderWithQueryClient( - , + , ); await waitFor(() => { diff --git a/ui/litellm-dashboard/src/components/OldTeams.tsx b/ui/litellm-dashboard/src/components/OldTeams.tsx index 4d045780c30..c7a2ae0e61a 100644 --- a/ui/litellm-dashboard/src/components/OldTeams.tsx +++ b/ui/litellm-dashboard/src/components/OldTeams.tsx @@ -54,13 +54,9 @@ import VectorStoreSelector from "./vector_store_management/VectorStoreSelector"; import SearchToolSelector from "./SearchTools/SearchToolSelector"; interface TeamProps { - teams: Team[] | null; - searchParams: any; accessToken: string | null; - setTeams: React.Dispatch>; userID: string | null; userRole: string | null; - organizations: Organization[] | null; premiumUser?: boolean; } @@ -165,18 +161,10 @@ const getOrganizationAlias = ( }; // @deprecated -const Teams: React.FC = ({ - teams, - searchParams, - accessToken, - setTeams, - userID, - userRole, - organizations, - premiumUser = false, -}) => { - console.log(`organizations: ${JSON.stringify(organizations)}`); +const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser = false }) => { const { data: organizationsData } = useOrganizations(); + const organizations = organizationsData ?? null; + const [teams, setTeams] = useState(null); const [isLoading, setIsLoading] = useState(true); const [fetchError, setFetchError] = useState(null); const [currentPage, setCurrentPage] = useState(1); @@ -721,7 +709,7 @@ const Teams: React.FC = ({ width: 160, ellipsis: true, render: (_: unknown, record: Team) => { - const orgAlias = getOrganizationAlias(record.organization_id, organizationsData || organizations); + const orgAlias = getOrganizationAlias(record.organization_id, organizations); return record.organization_id ? ( {orgAlias} @@ -860,7 +848,7 @@ const Teams: React.FC = ({ ), }, ], - [userRole, perTeamInfo, organizationsData, organizations], + [userRole, perTeamInfo, organizations], ); const displayTeams = useMemo(() => teams ?? [], [teams]); diff --git a/ui/litellm-dashboard/src/utils/migratedPages.test.ts b/ui/litellm-dashboard/src/utils/migratedPages.test.ts index f2e0f4eb151..365b03998fd 100644 --- a/ui/litellm-dashboard/src/utils/migratedPages.test.ts +++ b/ui/litellm-dashboard/src/utils/migratedPages.test.ts @@ -126,6 +126,13 @@ describe("migratedHref / legacyPageHref", () => { expect(MIGRATED_PAGES.users).toBe("users"); }); + + it("maps the teams id to its route", async () => { + vi.doMock("@/components/networking", () => ({ serverRootPath: "/" })); + const { MIGRATED_PAGES } = await import("./migratedPages"); + + expect(MIGRATED_PAGES.teams).toBe("teams"); + }); }); describe("dev server (NODE_ENV=development)", () => { diff --git a/ui/litellm-dashboard/src/utils/migratedPages.ts b/ui/litellm-dashboard/src/utils/migratedPages.ts index 337018f6ec5..7c07424a67c 100644 --- a/ui/litellm-dashboard/src/utils/migratedPages.ts +++ b/ui/litellm-dashboard/src/utils/migratedPages.ts @@ -43,6 +43,7 @@ export const MIGRATED_PAGES: Record = { agents: "agents", "router-settings": "router-settings", users: "users", + teams: "teams", }; function uiBase(): string {