diff --git a/ui/litellm-dashboard/e2e_tests/fixtures/migratedPages.ts b/ui/litellm-dashboard/e2e_tests/fixtures/migratedPages.ts index 00c4982529e..154badac021 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", organizations: "organizations", }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/page.tsx index 2369e130eee..c99b6eb9b40 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 { CreateKeyPrefillData } from "@/components/organisms/create_key_button"; import { fetchOrganizations } from "@/components/organizations"; import PassThroughSettings from "@/components/pass_through_settings"; @@ -318,17 +317,6 @@ function CreateKeyPageContent() { premiumUser={premiumUser} teams={teams} /> - ) : page == "teams" ? ( - ) : page == "pass-through-settings" ? ( ; +} diff --git a/ui/litellm-dashboard/src/components/OldTeams.test.tsx b/ui/litellm-dashboard/src/components/OldTeams.test.tsx index 7447432b876..4b076bbfb3c 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, + }); + 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 }); + 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 }); + 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, + }); + 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, + }); + 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, + }); + 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, + }); + 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, + }); + 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, + }); + renderWithQueryClient(); expect(screen.queryByRole("tab", { name: "Default Team Settings" })).not.toBeInTheDocument(); }); @@ -793,24 +754,15 @@ describe("OldTeams - access_group_ids in team create", () => { keys: [], members_with_roles: [], spend: 0, - } as any); + }); mockUseOrganizations.mockReturnValue({ data: [{ organization_id: "org-1", organization_alias: "Org 1", models: [], members: [] }], }); }); 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 }); + 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 }); + 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, + }); + 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, + }); + 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, + 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, + }); + 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, + }, + ], + total: 1, + page: 1, + page_size: 100, + total_pages: 1, + }); 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" }, { token: "t2" }], + members_with_roles: [], + spend: 0, + }, + ], + total: 1, + page: 1, + page_size: 100, + total_pages: 1, + }); 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 3afd5a81a9a..c3cbb72161a 100644 --- a/ui/litellm-dashboard/src/utils/migratedPages.test.ts +++ b/ui/litellm-dashboard/src/utils/migratedPages.test.ts @@ -127,6 +127,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"); + }); + it("maps the organizations id to its route", async () => { vi.doMock("@/components/networking", () => ({ serverRootPath: "/" })); const { MIGRATED_PAGES } = await import("./migratedPages"); diff --git a/ui/litellm-dashboard/src/utils/migratedPages.ts b/ui/litellm-dashboard/src/utils/migratedPages.ts index e9e6c527513..9c08aa1960b 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", organizations: "organizations", };