From 40549bc361cb1a3fc587d0d7a30d1f3726e4e04a Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 23 Dec 2025 20:15:42 -0800 Subject: [PATCH] Deprecate useTeam in favor of react query --- .../app/(dashboard)/hooks/teams/useTeams.ts | 17 ++++ .../src/app/(dashboard)/hooks/useTeams.tsx | 4 + .../organization/organization_view.test.tsx | 78 +++++++++++++++++++ .../organization/organization_view.tsx | 9 ++- .../src/utils/teamUtils.test.ts | 31 +++++++- ui/litellm-dashboard/src/utils/teamUtils.ts | 24 +++++- 6 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/teams/useTeams.ts diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/teams/useTeams.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/teams/useTeams.ts new file mode 100644 index 0000000000..8fb494539b --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/teams/useTeams.ts @@ -0,0 +1,17 @@ +import { useQuery, UseQueryResult } from "@tanstack/react-query"; +import { Team } from "@/components/key_team_helpers/key_list"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { fetchTeams } from "@/app/(dashboard)/networking"; +import { createQueryKeys } from "@/app/(dashboard)/hooks/common/queryKeysFactory"; + +const teamKeys = createQueryKeys("teams"); + +export const useTeams = (): UseQueryResult => { + const { accessToken, userId: userID, userRole } = useAuthorized(); + + return useQuery({ + queryKey: teamKeys.list({}), + queryFn: async () => await fetchTeams(accessToken!, userID, userRole, null), + enabled: Boolean(accessToken), + }); +}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useTeams.tsx b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useTeams.tsx index 64cbf624f9..0b3768505f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useTeams.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useTeams.tsx @@ -3,6 +3,10 @@ import { Team } from "@/components/key_team_helpers/key_list"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { fetchTeams } from "@/app/(dashboard)/networking"; +/** + * @deprecated This hook is deprecated. Use the react-query implementation from `@/app/(dashboard)/hooks/teams/useTeams` instead. + * This version will be removed in a future release. + */ const useTeams = () => { const [teams, setTeams] = useState([]); const { accessToken, userId: userID, userRole } = useAuthorized(); diff --git a/ui/litellm-dashboard/src/components/organization/organization_view.test.tsx b/ui/litellm-dashboard/src/components/organization/organization_view.test.tsx index 5204efc941..4320ad6c69 100644 --- a/ui/litellm-dashboard/src/components/organization/organization_view.test.tsx +++ b/ui/litellm-dashboard/src/components/organization/organization_view.test.tsx @@ -40,6 +40,24 @@ vi.mock("../mcp_server_management/MCPServerSelector", () => ({ __esModule: true, default: () => null, })); +const mockUseTeamsData = { + data: [ + { + team_id: "team_123", + team_alias: "Engineering Team", + }, + { + team_id: "team_456", + team_alias: "Marketing Team", + }, + ], +}; + +const mockUseTeams = vi.fn(() => mockUseTeamsData); + +vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({ + useTeams: () => mockUseTeams(), +})); const mockOrg = { organization_alias: "Acme Corp", @@ -103,3 +121,63 @@ test("should display empty state when organization has no members", async () => expect(screen.getByText("No members found")).toBeInTheDocument(); }); }); + +test("should display team aliases when teams are available", async () => { + const { organizationInfoCall } = await import("../networking"); + const orgWithTeams = { + ...mockOrg, + teams: [{ team_id: "team_123" }, { team_id: "team_456" }], + }; + (organizationInfoCall as unknown as ReturnType).mockResolvedValueOnce(orgWithTeams); + + render( + {}} + accessToken="test-token" + is_org_admin={false} + is_proxy_admin={false} + userModels={[]} + editOrg={false} + />, + ); + + await waitFor(() => { + expect(screen.getByText("Engineering Team")).toBeInTheDocument(); + expect(screen.getByText("Marketing Team")).toBeInTheDocument(); + }); +}); + +test("should display team ID as fallback when alias is not found", async () => { + const { organizationInfoCall } = await import("../networking"); + mockUseTeams.mockReturnValueOnce({ + data: [ + { + team_id: "team_123", + team_alias: "Engineering Team", + }, + ], + }); + + const orgWithUnknownTeam = { + ...mockOrg, + teams: [{ team_id: "team_999" }], + }; + (organizationInfoCall as unknown as ReturnType).mockResolvedValueOnce(orgWithUnknownTeam); + + render( + {}} + accessToken="test-token" + is_org_admin={false} + is_proxy_admin={false} + userModels={[]} + editOrg={false} + />, + ); + + await waitFor(() => { + expect(screen.getByText("team_999")).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/organization/organization_view.tsx b/ui/litellm-dashboard/src/components/organization/organization_view.tsx index 595987aadf..91b2b4ce59 100644 --- a/ui/litellm-dashboard/src/components/organization/organization_view.tsx +++ b/ui/litellm-dashboard/src/components/organization/organization_view.tsx @@ -23,7 +23,7 @@ import { } from "@tremor/react"; import { Button, Form, Input, Select } from "antd"; import { CheckIcon, CopyIcon } from "lucide-react"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useMemo } from "react"; import UserSearchModal from "../common_components/user_search_modal"; import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key"; import MCPServerSelector from "../mcp_server_management/MCPServerSelector"; @@ -41,6 +41,8 @@ import ObjectPermissionsView from "../object_permissions_view"; import NumericalInput from "../shared/numerical_input"; import MemberModal from "../team/EditMembership"; import VectorStoreSelector from "../vector_store_management/VectorStoreSelector"; +import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams"; +import { createTeamAliasMap } from "@/utils/teamUtils"; interface OrganizationInfoProps { organizationId: string; @@ -71,6 +73,9 @@ const OrganizationInfoView: React.FC = ({ const [copiedStates, setCopiedStates] = useState>({}); const [isOrgSaving, setIsOrgSaving] = useState(false); const canEditOrg = is_org_admin || is_proxy_admin; + const { data: teams } = useTeams(); + + const teamAliasMap = useMemo(() => createTeamAliasMap(teams), [teams]); const fetchOrgInfo = async () => { try { @@ -310,7 +315,7 @@ const OrganizationInfoView: React.FC = ({
{orgData.teams?.map((team, index) => ( - {team.team_id} + {teamAliasMap[team.team_id] || team.team_id} ))}
diff --git a/ui/litellm-dashboard/src/utils/teamUtils.test.ts b/ui/litellm-dashboard/src/utils/teamUtils.test.ts index 1151d53261..6c82da6185 100644 --- a/ui/litellm-dashboard/src/utils/teamUtils.test.ts +++ b/ui/litellm-dashboard/src/utils/teamUtils.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { resolveTeamAliasFromTeamID } from "./teamUtils"; -import type { Team } from "@/components/networking"; +import { resolveTeamAliasFromTeamID, createTeamAliasMap } from "./teamUtils"; +import type { Team } from "@/components/key_team_helpers/key_list"; describe("resolveTeamAliasFromTeamID", () => { it("should return team alias when team is found", () => { @@ -35,3 +35,30 @@ describe("resolveTeamAliasFromTeamID", () => { expect(result).toBeNull(); }); }); + +describe("createTeamAliasMap", () => { + it("should create a map from team_id to team_alias", () => { + const teams = [ + { + team_id: "team1", + team_alias: "Team One", + }, + { + team_id: "team2", + team_alias: "Team Two", + }, + ] as unknown as Team[]; + + const result = createTeamAliasMap(teams); + expect(result).toEqual({ + team1: "Team One", + team2: "Team Two", + }); + }); + + it("should return empty object when teams is null or undefined", () => { + expect(createTeamAliasMap(null)).toEqual({}); + expect(createTeamAliasMap(undefined)).toEqual({}); + expect(createTeamAliasMap([])).toEqual({}); + }); +}); diff --git a/ui/litellm-dashboard/src/utils/teamUtils.ts b/ui/litellm-dashboard/src/utils/teamUtils.ts index 1916e1c98a..67100661a6 100644 --- a/ui/litellm-dashboard/src/utils/teamUtils.ts +++ b/ui/litellm-dashboard/src/utils/teamUtils.ts @@ -1,5 +1,27 @@ -import { Team } from "@/components/networking"; +import { Team } from "@/components/key_team_helpers/key_list"; +/** + * Creates a map from team_id to team_alias for efficient lookups. + * @param teams - Array of Team objects + * @returns Record mapping team_id to team_alias + */ +export const createTeamAliasMap = (teams: Team[] | null | undefined): Record => { + if (!teams) return {}; + return teams.reduce( + (acc, team) => { + acc[team.team_id] = team.team_alias; + return acc; + }, + {} as Record, + ); +}; + +/** + * Resolves a team alias from a team ID. + * @param teamID - The team ID to look up + * @param teams - Array of Team objects + * @returns The team alias if found, null otherwise + */ export const resolveTeamAliasFromTeamID = (teamID: string, teams: Team[]): string | null => { const team = teams.find((team) => team.team_id === teamID); return team ? team.team_alias : null;