diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx
index 8bfbaa8d3a6..9d77774cb4c 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx
@@ -26,7 +26,7 @@ import ModelGroupAliasSettings from "../../../components/model_group_alias_setti
import ModelInfoView from "../../../components/model_info_view";
import NotificationsManager from "../../../components/molecules/notifications_manager";
import PassThroughSettings from "../../../components/pass_through_settings";
-import TeamInfoView from "../../../components/team/team_info";
+import TeamInfoView from "../../../components/team/TeamInfo";
import useAuthorized from "../hooks/useAuthorized";
interface ModelDashboardProps {
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/TeamsView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/TeamsView.tsx
index 10616e95523..88bdf3cdda0 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/teams/TeamsView.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/TeamsView.tsx
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
import { teamDeleteCall, Organization } from "@/components/networking";
import { fetchTeams } from "@/components/common_components/fetch_teams";
import { Form } from "antd";
-import TeamInfoView from "@/components/team/team_info";
+import TeamInfoView from "@/components/team/TeamInfo";
import TeamSSOSettings from "@/components/TeamSSOSettings";
import { isAdminRole } from "@/utils/roles";
import { Card, Button, Col, Text, Grid, TabPanel } from "@tremor/react";
diff --git a/ui/litellm-dashboard/src/components/OldTeams.test.tsx b/ui/litellm-dashboard/src/components/OldTeams.test.tsx
index 91b428c8c98..0ca37cc82d7 100644
--- a/ui/litellm-dashboard/src/components/OldTeams.test.tsx
+++ b/ui/litellm-dashboard/src/components/OldTeams.test.tsx
@@ -52,7 +52,7 @@ vi.mock("./key_team_helpers/fetch_available_models_team_key", () => ({
}),
}));
-vi.mock("@/components/team/team_info", () => ({
+vi.mock("@/components/team/TeamInfo", () => ({
__esModule: true,
default: (props: any) => {
mockTeamInfoView(props);
diff --git a/ui/litellm-dashboard/src/components/OldTeams.tsx b/ui/litellm-dashboard/src/components/OldTeams.tsx
index 4eab197db14..ecc6a624be0 100644
--- a/ui/litellm-dashboard/src/components/OldTeams.tsx
+++ b/ui/litellm-dashboard/src/components/OldTeams.tsx
@@ -1,6 +1,6 @@
import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
import AvailableTeamsPanel from "@/components/team/available_teams";
-import TeamInfoView from "@/components/team/team_info";
+import TeamInfoView from "@/components/team/TeamInfo";
import TeamSSOSettings from "@/components/TeamSSOSettings";
import { isProxyAdminRole } from "@/utils/roles";
import { InfoCircleOutlined } from "@ant-design/icons";
diff --git a/ui/litellm-dashboard/src/components/team/team_info.test.tsx b/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx
similarity index 88%
rename from ui/litellm-dashboard/src/components/team/team_info.test.tsx
rename to ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx
index fa1b13fbd2d..7a15a6e3f22 100644
--- a/ui/litellm-dashboard/src/components/team/team_info.test.tsx
+++ b/ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx
@@ -3,7 +3,7 @@ import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../../../tests/test-utils";
-import TeamInfoView from "./team_info";
+import TeamInfoView from "./TeamInfo";
vi.mock("@/components/networking", () => ({
teamInfoCall: vi.fn(),
@@ -40,7 +40,7 @@ vi.mock("@/app/(dashboard)/hooks/users/useCurrentUser", () => ({
useCurrentUser: vi.fn(),
}));
-vi.mock("@/components/team/team_member_view", () => ({
+vi.mock("@/components/team/TeamMemberTab", () => ({
default: vi.fn(({ setIsAddMemberModalVisible }) => (
@@ -89,10 +89,6 @@ vi.mock("@/components/team/member_permissions", () => ({
default: vi.fn(() =>
Member Permissions
),
}));
-vi.mock("@/components/team/member_permissions", () => ({
- default: vi.fn(() =>
Member Permissions
),
-}));
-
import { useAllProxyModels } from "@/app/(dashboard)/hooks/models/useModels";
import { useOrganization } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
import { useTeam } from "@/app/(dashboard)/hooks/teams/useTeams";
@@ -335,10 +331,10 @@ describe("TeamInfoView", () => {
await user.click(settingsTab);
await waitFor(() => {
- expect(screen.getByRole("button", { name: "Edit Settings" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument();
});
- const editButton = screen.getByRole("button", { name: "Edit Settings" });
+ const editButton = screen.getByRole("button", { name: /edit settings/i });
await user.click(editButton);
await waitFor(() => {
@@ -361,17 +357,17 @@ describe("TeamInfoView", () => {
await user.click(settingsTab);
await waitFor(() => {
- expect(screen.getByRole("button", { name: "Edit Settings" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument();
});
- const editButton = screen.getByRole("button", { name: "Edit Settings" });
+ const editButton = screen.getByRole("button", { name: /edit settings/i });
await user.click(editButton);
await waitFor(() => {
expect(screen.getByLabelText("Team Name")).toBeInTheDocument();
});
- const cancelButton = screen.getByRole("button", { name: "Cancel" });
+ const cancelButton = screen.getByRole("button", { name: /cancel/i });
await user.click(cancelButton);
await waitFor(() => {
@@ -438,10 +434,10 @@ describe("TeamInfoView", () => {
await user.click(settingsTab);
await waitFor(() => {
- expect(screen.getByRole("button", { name: "Edit Settings" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument();
});
- const editButton = screen.getByRole("button", { name: "Edit Settings" });
+ const editButton = screen.getByRole("button", { name: /edit settings/i });
await user.click(editButton);
const secretField = await screen.findByPlaceholderText(
@@ -472,10 +468,10 @@ describe("TeamInfoView", () => {
await user.click(settingsTab);
await waitFor(() => {
- expect(screen.getByRole("button", { name: "Edit Settings" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument();
});
- const editButton = screen.getByRole("button", { name: "Edit Settings" });
+ const editButton = screen.getByRole("button", { name: /edit settings/i });
await user.click(editButton);
const secretField = await screen.findByPlaceholderText(
@@ -502,10 +498,10 @@ describe("TeamInfoView", () => {
await user.click(membersTab);
await waitFor(() => {
- expect(screen.getByRole("button", { name: "Add Member" })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /add member/i })).toBeInTheDocument();
});
- const addButton = screen.getByRole("button", { name: "Add Member" });
+ const addButton = screen.getByRole("button", { name: /add member/i });
await user.click(addButton);
await waitFor(() => {
@@ -603,6 +599,47 @@ describe("TeamInfoView", () => {
});
});
+ it("should open Settings tab by default when editTeam is true and user can edit", async () => {
+ vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData());
+
+ renderWithProviders(
);
+
+ await waitFor(() => {
+ const teamNameElements = screen.queryAllByText("Test Team");
+ expect(teamNameElements.length).toBeGreaterThan(0);
+ });
+
+ expect(screen.getByText("Team Settings")).toBeInTheDocument();
+ });
+
+ it("should open Overview tab by default when editTeam is false", async () => {
+ vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData());
+
+ renderWithProviders(
);
+
+ await waitFor(() => {
+ const teamNameElements = screen.queryAllByText("Test Team");
+ expect(teamNameElements.length).toBeGreaterThan(0);
+ });
+
+ expect(screen.getByText("Budget Status")).toBeInTheDocument();
+ });
+
+ it("should open Overview tab by default when editTeam is true but user cannot edit", async () => {
+ vi.mocked(networking.teamInfoCall).mockResolvedValue(createMockTeamData());
+
+ renderWithProviders(
+
+ );
+
+ await waitFor(() => {
+ const teamNameElements = screen.queryAllByText("Test Team");
+ expect(teamNameElements.length).toBeGreaterThan(0);
+ });
+
+ expect(screen.getByText("Budget Status")).toBeInTheDocument();
+ });
+
it("should display soft budget alerting emails in settings view when present", async () => {
const user = userEvent.setup();
vi.mocked(networking.teamInfoCall).mockResolvedValue(
diff --git a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx
new file mode 100644
index 00000000000..d081538fb64
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx
@@ -0,0 +1,1294 @@
+import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
+import UserSearchModal from "@/components/common_components/user_search_modal";
+import {
+ getGuardrailsList,
+ getPoliciesList,
+ getPolicyInfoWithGuardrails,
+ Member,
+ Organization,
+ organizationInfoCall,
+ teamInfoCall,
+ teamMemberAddCall,
+ teamMemberDeleteCall,
+ teamMemberUpdateCall,
+ teamUpdateCall,
+} from "@/components/networking";
+import { formatNumberWithCommas } from "@/utils/dataUtils";
+import { mapEmptyStringToNull } from "@/utils/keyUpdateUtils";
+import { isProxyAdminRole } from "@/utils/roles";
+import { EditOutlined, InfoCircleOutlined, SaveOutlined } from "@ant-design/icons";
+import { ArrowLeftIcon } from "@heroicons/react/outline";
+import { Badge, Card, Grid, Text, TextInput, Title } from "@tremor/react";
+import { Button, Form, Input, message, Select, Switch, Tabs, Tooltip } from "antd";
+import { CheckIcon, CopyIcon } from "lucide-react";
+import React, { useEffect, useMemo, useState } from "react";
+import { copyToClipboard as utilCopyToClipboard } from "../../utils/dataUtils";
+import AgentSelector from "../agent_management/AgentSelector";
+import DeleteResourceModal from "../common_components/DeleteResourceModal";
+import DurationSelect from "../common_components/DurationSelect";
+import PassThroughRoutesSelector from "../common_components/PassThroughRoutesSelector";
+import { unfurlWildcardModelsInList } from "../key_team_helpers/fetch_available_models_team_key";
+import LoggingSettingsView from "../logging_settings_view";
+import MCPServerSelector from "../mcp_server_management/MCPServerSelector";
+import MCPToolPermissions from "../mcp_server_management/MCPToolPermissions";
+import { ModelSelect } from "../ModelSelect/ModelSelect";
+import NotificationsManager from "../molecules/notifications_manager";
+import { fetchMCPAccessGroups } from "../networking";
+import ObjectPermissionsView from "../object_permissions_view";
+import NumericalInput from "../shared/numerical_input";
+import VectorStoreSelector from "../vector_store_management/VectorStoreSelector";
+import EditLoggingSettings from "./EditLoggingSettings";
+import MemberModal from "./EditMembership";
+import MemberPermissions from "./member_permissions";
+import {
+ getTeamInfoDefaultTab,
+ getTeamInfoVisibleTabs,
+ TEAM_INFO_TAB_KEYS,
+ TEAM_INFO_TAB_LABELS,
+} from "./tabVisibilityUtils";
+import TeamMembersComponent from "./TeamMemberTab";
+
+export interface TeamMembership {
+ user_id: string;
+ team_id: string;
+ budget_id: string;
+ spend: number;
+ litellm_budget_table: {
+ budget_id: string;
+ soft_budget: number | null;
+ max_budget: number | null;
+ max_parallel_requests: number | null;
+ tpm_limit: number | null;
+ rpm_limit: number | null;
+ model_max_budget: Record
| null;
+ budget_duration: string | null;
+ };
+}
+
+export interface TeamData {
+ team_id: string;
+ team_info: {
+ team_alias: string;
+ team_id: string;
+ organization_id: string | null;
+ admins: string[];
+ members: string[];
+ members_with_roles: Member[];
+ metadata: Record;
+ tpm_limit: number | null;
+ rpm_limit: number | null;
+ max_budget: number | null;
+ soft_budget?: number | null;
+ budget_duration: string | null;
+ models: string[];
+ blocked: boolean;
+ spend: number;
+ max_parallel_requests: number | null;
+ budget_reset_at: string | null;
+ model_id: string | null;
+ litellm_model_table: {
+ model_aliases: Record;
+ } | null;
+ created_at: string;
+ guardrails?: string[];
+ policies?: string[];
+ object_permission?: {
+ object_permission_id: string;
+ mcp_servers: string[];
+ mcp_access_groups?: string[];
+ mcp_tool_permissions?: Record;
+ vector_stores: string[];
+ agents?: string[];
+ agent_access_groups?: string[];
+ };
+ team_member_budget_table: {
+ max_budget: number;
+ budget_duration: string;
+ tpm_limit: number | null;
+ rpm_limit: number | null;
+ } | null;
+ };
+ keys: any[];
+ team_memberships: TeamMembership[];
+}
+
+export interface TeamInfoProps {
+ teamId: string;
+ onUpdate: (data: any) => void;
+ onClose: () => void;
+ accessToken: string | null;
+ is_team_admin: boolean;
+ is_proxy_admin: boolean;
+ userModels: string[];
+ editTeam: boolean;
+ premiumUser?: boolean;
+}
+
+const getOrganizationModels = (organization: Organization | null, userModels: string[]) => {
+ let tempModelsToPick = [];
+
+ if (organization) {
+ // Check if organization has "all-proxy-models" in its models array
+ if (organization.models.includes("all-proxy-models")) {
+ // Treat as all-proxy-models (use userModels)
+ tempModelsToPick = userModels;
+ } else if (organization.models.length > 0) {
+ // Organization has specific models
+ tempModelsToPick = organization.models;
+ } else {
+ // Empty array [] is treated as all-proxy-models
+ tempModelsToPick = userModels;
+ }
+ } else {
+ // No organization, show all available models
+ tempModelsToPick = userModels;
+ }
+
+ return unfurlWildcardModelsInList(tempModelsToPick, userModels);
+};
+
+const TeamInfoView: React.FC = ({
+ teamId,
+ onClose,
+ accessToken,
+ is_team_admin,
+ is_proxy_admin,
+ userModels,
+ editTeam,
+ premiumUser = false,
+ onUpdate,
+}) => {
+ const [teamData, setTeamData] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false);
+ const [form] = Form.useForm();
+ const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false);
+ const [selectedEditMember, setSelectedEditMember] = useState(null);
+ const [isEditing, setIsEditing] = useState(false);
+ const [mcpAccessGroups, setMcpAccessGroups] = useState([]);
+ const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false);
+ const [copiedStates, setCopiedStates] = useState>({});
+ const [guardrailsList, setGuardrailsList] = useState([]);
+ const [policiesList, setPoliciesList] = useState([]);
+ const [policyGuardrails, setPolicyGuardrails] = useState>({});
+ const [loadingPolicies, setLoadingPolicies] = useState(false);
+ const [memberToDelete, setMemberToDelete] = useState(null);
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+ const [isDeleting, setIsDeleting] = useState(false);
+ const [isTeamSaving, setIsTeamSaving] = useState(false);
+ const [organization, setOrganization] = useState(null);
+ const { userRole } = useAuthorized();
+
+ const canEditTeam = is_team_admin || is_proxy_admin;
+ const visibleTabs = useMemo(() => getTeamInfoVisibleTabs(canEditTeam), [canEditTeam]);
+ const defaultTabKey = useMemo(
+ () => getTeamInfoDefaultTab(editTeam, canEditTeam),
+ [editTeam, canEditTeam]
+ );
+
+ const fetchTeamInfo = async () => {
+ try {
+ setLoading(true);
+ if (!accessToken) return;
+ const response = await teamInfoCall(accessToken, teamId);
+ setTeamData(response);
+ } catch (error) {
+ NotificationsManager.fromBackend("Failed to load team information");
+ console.error("Error fetching team info:", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchTeamInfo();
+ }, [teamId, accessToken]);
+
+ // Fetch organization data when team has organization_id
+ useEffect(() => {
+ const fetchOrganization = async () => {
+ if (!accessToken || !teamData?.team_info?.organization_id) {
+ setOrganization(null);
+ return;
+ }
+
+ try {
+ const orgData = await organizationInfoCall(accessToken, teamData.team_info.organization_id);
+ setOrganization(orgData);
+ } catch (error) {
+ console.error("Error fetching organization info:", error);
+ setOrganization(null);
+ }
+ };
+
+ fetchOrganization();
+ }, [accessToken, teamData?.team_info?.organization_id]);
+
+ // Compute modelsToPick based on organization and userModels
+ const modelsToPick = useMemo(() => {
+ return getOrganizationModels(organization, userModels);
+ }, [organization, userModels]);
+
+ const fetchMcpAccessGroups = async () => {
+ if (!accessToken) return;
+ if (mcpAccessGroupsLoaded) return;
+ try {
+ const groups = await fetchMCPAccessGroups(accessToken);
+ setMcpAccessGroups(groups);
+ setMcpAccessGroupsLoaded(true);
+ } catch (error) {
+ console.error("Failed to fetch MCP access groups:", error);
+ }
+ };
+
+ useEffect(() => {
+ const fetchGuardrails = async () => {
+ try {
+ if (!accessToken) return;
+ const response = await getGuardrailsList(accessToken);
+ const guardrailNames = response.guardrails.map((g: { guardrail_name: string }) => g.guardrail_name);
+ setGuardrailsList(guardrailNames);
+ } catch (error) {
+ console.error("Failed to fetch guardrails:", error);
+ }
+ };
+
+ const fetchPolicies = async () => {
+ try {
+ if (!accessToken) return;
+ const response = await getPoliciesList(accessToken);
+ const policyNames = response.policies.map((p: { policy_name: string }) => p.policy_name);
+ setPoliciesList(policyNames);
+ } catch (error) {
+ console.error("Failed to fetch policies:", error);
+ }
+ };
+
+ fetchGuardrails();
+ fetchPolicies();
+ }, [accessToken]);
+
+ // Fetch resolved guardrails for all policies
+ useEffect(() => {
+ const fetchPolicyGuardrails = async () => {
+ if (!accessToken || !teamData?.team_info?.policies || teamData.team_info.policies.length === 0) {
+ return;
+ }
+
+ setLoadingPolicies(true);
+ const guardrailsMap: Record = {};
+
+ try {
+ await Promise.all(
+ teamData.team_info.policies.map(async (policyName: string) => {
+ try {
+ const policyInfo = await getPolicyInfoWithGuardrails(accessToken, policyName);
+ guardrailsMap[policyName] = policyInfo.resolved_guardrails || [];
+ } catch (error) {
+ console.error(`Failed to fetch guardrails for policy ${policyName}:`, error);
+ guardrailsMap[policyName] = [];
+ }
+ })
+ );
+ setPolicyGuardrails(guardrailsMap);
+ } catch (error) {
+ console.error("Failed to fetch policy guardrails:", error);
+ } finally {
+ setLoadingPolicies(false);
+ }
+ };
+
+ fetchPolicyGuardrails();
+ }, [accessToken, teamData?.team_info?.policies]);
+
+ const handleMemberCreate = async (values: any) => {
+ try {
+ if (accessToken == null) return;
+
+ const member: Member = {
+ user_email: values.user_email,
+ user_id: values.user_id,
+ role: values.role,
+ };
+
+ await teamMemberAddCall(accessToken, teamId, member);
+
+ NotificationsManager.success("Team member added successfully");
+ setIsAddMemberModalVisible(false);
+ form.resetFields();
+
+ // Fetch updated team info
+ const updatedTeamData = await teamInfoCall(accessToken, teamId);
+ setTeamData(updatedTeamData);
+
+ // Notify parent component of the update
+ onUpdate(updatedTeamData);
+ } catch (error: any) {
+ let errMsg = "Failed to add team member";
+
+ if (error?.raw?.detail?.error?.includes("Assigning team admins is a premium feature")) {
+ errMsg = "Assigning admins is an enterprise-only feature. Please upgrade your LiteLLM plan to enable this.";
+ } else if (error?.message) {
+ errMsg = error.message;
+ }
+
+ NotificationsManager.fromBackend(errMsg);
+ console.error("Error adding team member:", error);
+ }
+ };
+
+ const handleMemberUpdate = async (values: any) => {
+ try {
+ if (accessToken == null) {
+ return;
+ }
+
+ const member: Member = {
+ user_email: values.user_email,
+ user_id: values.user_id,
+ role: values.role,
+ max_budget_in_team: values.max_budget_in_team,
+ tpm_limit: values.tpm_limit,
+ rpm_limit: values.rpm_limit,
+ };
+ message.destroy(); // Remove all existing toasts
+
+ await teamMemberUpdateCall(accessToken, teamId, member);
+
+ NotificationsManager.success("Team member updated successfully");
+ setIsEditMemberModalVisible(false);
+
+ // Fetch updated team info
+ const updatedTeamData = await teamInfoCall(accessToken, teamId);
+ setTeamData(updatedTeamData);
+
+ // Notify parent component of the update
+ onUpdate(updatedTeamData);
+ } catch (error: any) {
+ let errMsg = "Failed to update team member";
+ if (error?.raw?.detail?.includes("Assigning team admins is a premium feature")) {
+ errMsg = "Assigning admins is an enterprise-only feature. Please upgrade your LiteLLM plan to enable this.";
+ } else if (error?.message) {
+ errMsg = error.message;
+ }
+ setIsEditMemberModalVisible(false);
+
+ message.destroy(); // Remove all existing toasts
+
+ NotificationsManager.fromBackend(errMsg);
+ console.error("Error updating team member:", error);
+ }
+ };
+
+ const handleMemberDelete = (member: Member) => {
+ setMemberToDelete(member);
+ setIsDeleteModalOpen(true);
+ };
+
+ const handleDeleteConfirm = async () => {
+ if (!memberToDelete || !accessToken) return;
+
+ setIsDeleting(true);
+ try {
+ await teamMemberDeleteCall(accessToken, teamId, memberToDelete);
+
+ NotificationsManager.success("Team member removed successfully");
+
+ // Fetch updated team info
+ const updatedTeamData = await teamInfoCall(accessToken, teamId);
+ setTeamData(updatedTeamData);
+
+ // Notify parent component of the update
+ onUpdate(updatedTeamData);
+ } catch (error) {
+ NotificationsManager.fromBackend("Failed to remove team member");
+ console.error("Error removing team member:", error);
+ } finally {
+ setIsDeleting(false);
+ setIsDeleteModalOpen(false);
+ setMemberToDelete(null);
+ }
+ };
+
+ const handleDeleteCancel = () => {
+ setIsDeleteModalOpen(false);
+ setMemberToDelete(null);
+ };
+
+ const handleTeamUpdate = async (values: any) => {
+ try {
+ if (!accessToken) return;
+ setIsTeamSaving(true);
+
+ let parsedMetadata = {};
+ try {
+ const rawMetadata = values.metadata ? JSON.parse(values.metadata) : {};
+ // Exclude soft_budget_alerting_emails from parsed metadata since it's handled separately
+ const { soft_budget_alerting_emails, ...rest } = rawMetadata;
+ parsedMetadata = rest;
+ } catch (e) {
+ NotificationsManager.fromBackend("Invalid JSON in metadata field");
+ return;
+ }
+
+ let secretManagerSettings: Record | undefined;
+ if (typeof values.secret_manager_settings === "string") {
+ const trimmedSecretConfig = values.secret_manager_settings.trim();
+ if (trimmedSecretConfig.length > 0) {
+ try {
+ secretManagerSettings = JSON.parse(values.secret_manager_settings);
+ } catch (e) {
+ NotificationsManager.fromBackend("Invalid JSON in secret manager settings");
+ return;
+ }
+ }
+ }
+
+ const sanitizeNumeric = (v: any) => {
+ if (v === null || v === undefined) return null;
+ if (typeof v === "string" && v.trim() === "") return null;
+ if (typeof v === "number" && Number.isNaN(v)) return null;
+ return v;
+ };
+
+ const updateData: any = {
+ team_id: teamId,
+ team_alias: values.team_alias,
+ models: values.models,
+ tpm_limit: sanitizeNumeric(values.tpm_limit),
+ rpm_limit: sanitizeNumeric(values.rpm_limit),
+ max_budget: values.max_budget,
+ soft_budget: sanitizeNumeric(values.soft_budget),
+ budget_duration: values.budget_duration,
+ metadata: {
+ ...parsedMetadata,
+ ...(values.guardrails?.length > 0 ? { guardrails: values.guardrails } : {}),
+ ...(values.logging_settings?.length > 0 ? { logging: values.logging_settings } : {}),
+ disable_global_guardrails: values.disable_global_guardrails || false,
+ soft_budget_alerting_emails:
+ typeof values.soft_budget_alerting_emails === "string"
+ ? values.soft_budget_alerting_emails
+ .split(",")
+ .map((email: string) => email.trim())
+ .filter((email: string) => email.length > 0)
+ : values.soft_budget_alerting_emails || [],
+ ...(secretManagerSettings !== undefined ? { secret_manager_settings: secretManagerSettings } : {}),
+ },
+ ...(values.policies?.length > 0 ? { policies: values.policies } : {}),
+ organization_id: values.organization_id,
+ };
+
+ updateData.max_budget = mapEmptyStringToNull(updateData.max_budget);
+ updateData.team_member_budget_duration = values.team_member_budget_duration;
+
+ if (values.team_member_budget !== undefined) {
+ updateData.team_member_budget = Number(values.team_member_budget);
+ }
+
+ if (values.team_member_key_duration !== undefined) {
+ updateData.team_member_key_duration = values.team_member_key_duration;
+ }
+
+ if (values.team_member_tpm_limit !== undefined || values.team_member_rpm_limit !== undefined) {
+ updateData.team_member_tpm_limit = sanitizeNumeric(values.team_member_tpm_limit);
+ updateData.team_member_rpm_limit = sanitizeNumeric(values.team_member_rpm_limit);
+ }
+
+ // Handle object_permission updates
+ const { servers, accessGroups } = values.mcp_servers_and_groups || {
+ servers: [],
+ accessGroups: [],
+ };
+ const serverIds = new Set(servers || []);
+ const mcpToolPermissions = Object.fromEntries(
+ Object.entries(values.mcp_tool_permissions || {}).filter(([serverId]) => serverIds.has(serverId)),
+ );
+
+ updateData.object_permission = {};
+ if (servers) {
+ updateData.object_permission.mcp_servers = servers;
+ }
+ if (accessGroups) {
+ updateData.object_permission.mcp_access_groups = accessGroups;
+ }
+ if (mcpToolPermissions) {
+ updateData.object_permission.mcp_tool_permissions = mcpToolPermissions;
+ }
+ delete values.mcp_servers_and_groups;
+ delete values.mcp_tool_permissions;
+
+ // Handle agent permissions
+ const { agents, accessGroups: agentAccessGroups } = values.agents_and_groups || {
+ agents: [],
+ accessGroups: [],
+ };
+ if (agents && agents.length > 0) {
+ updateData.object_permission.agents = agents;
+ }
+ if (agentAccessGroups && agentAccessGroups.length > 0) {
+ updateData.object_permission.agent_access_groups = agentAccessGroups;
+ }
+ delete values.agents_and_groups;
+
+ // Handle vector stores permissions
+ if (values.vector_stores && values.vector_stores.length > 0) {
+ updateData.object_permission.vector_stores = values.vector_stores;
+ }
+
+ const response = await teamUpdateCall(accessToken, updateData);
+
+ NotificationsManager.success("Team settings updated successfully");
+ setIsEditing(false);
+ fetchTeamInfo();
+ } catch (error) {
+ console.error("Error updating team:", error);
+ } finally {
+ setIsTeamSaving(false);
+ }
+ };
+
+ if (loading) {
+ return Loading...
;
+ }
+
+ if (!teamData?.team_info) {
+ return Team not found
;
+ }
+
+ const { team_info: info } = teamData;
+
+ const copyToClipboard = async (text: string, key: string) => {
+ const success = await utilCopyToClipboard(text);
+ if (success) {
+ setCopiedStates((prev) => ({ ...prev, [key]: true }));
+ setTimeout(() => {
+ setCopiedStates((prev) => ({ ...prev, [key]: false }));
+ }, 2000);
+ }
+ };
+
+ return (
+
+
+
+
}
+ onClick={onClose}
+ className="mb-4"
+ >
+ Back to Teams
+
+
{info.team_alias}
+
+ {info.team_id}
+ : }
+ onClick={() => copyToClipboard(info.team_id, "team-id")}
+ className={`left-2 z-10 transition-all duration-200 ${copiedStates["team-id"]
+ ? "text-green-600 bg-green-50 border-green-200"
+ : "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
+ }`}
+ />
+
+
+
+
+
+
+ Budget Status
+
+
${formatNumberWithCommas(info.spend, 4)}
+
+ of {info.max_budget === null ? "Unlimited" : `$${formatNumberWithCommas(info.max_budget, 4)}`}
+
+ {info.budget_duration && Reset: {info.budget_duration}}
+
+ {info.team_member_budget_table && (
+
+ Team Member Budget: ${formatNumberWithCommas(info.team_member_budget_table.max_budget, 4)}
+
+ )}
+
+
+
+
+ Rate Limits
+
+ TPM: {info.tpm_limit || "Unlimited"}
+ RPM: {info.rpm_limit || "Unlimited"}
+ {info.max_parallel_requests && Max Parallel Requests: {info.max_parallel_requests}}
+
+
+
+
+ Models
+
+ {info.models.length === 0 ? (
+ All proxy models
+ ) : (
+ info.models.map((model, index) => (
+
+ {model}
+
+ ))
+ )}
+
+
+
+
+ Virtual Keys
+
+ User Keys: {teamData.keys.filter((key) => key.user_id).length}
+ Service Account Keys: {teamData.keys.filter((key) => !key.user_id).length}
+ Total: {teamData.keys.length}
+
+
+
+
+
+
+ Guardrails
+ {info.guardrails && info.guardrails.length > 0 ? (
+
+ {info.guardrails.map((guardrail: string, index: number) => (
+
+ {guardrail}
+
+ ))}
+
+ ) : (
+ No guardrails configured
+ )}
+ {info.metadata?.disable_global_guardrails && (
+
+ Global Guardrails Disabled
+
+ )}
+
+
+
+ Policies
+ {info.policies && info.policies.length > 0 ? (
+
+ {info.policies.map((policy: string, index: number) => (
+
+
+ {policy}
+ {loadingPolicies && Loading guardrails...}
+
+ {!loadingPolicies && policyGuardrails[policy] && policyGuardrails[policy].length > 0 && (
+
+
Resolved Guardrails:
+
+ {policyGuardrails[policy].map((guardrail: string, gIndex: number) => (
+
+ {guardrail}
+
+ ))}
+
+
+ )}
+
+ ))}
+
+ ) : (
+ No policies configured
+ )}
+
+
+
+
+ ),
+ },
+ {
+ key: TEAM_INFO_TAB_KEYS.MEMBERS,
+ label: TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.MEMBERS],
+ children: (
+
+ ),
+ },
+ {
+ key: TEAM_INFO_TAB_KEYS.MEMBER_PERMISSIONS,
+ label: TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.MEMBER_PERMISSIONS],
+ children: (
+
+ ),
+ },
+ {
+ key: TEAM_INFO_TAB_KEYS.SETTINGS,
+ label: TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.SETTINGS],
+ children: (
+
+
+
Team Settings
+ {canEditTeam && !isEditing && (
+ } onClick={() => setIsEditing(true)}>Edit Settings
+ )}
+
+
+ {isEditing ? (
+
+
+
+
+
+ form.setFieldValue("models", values)}
+ teamID={teamId}
+ organizationID={teamData?.team_info?.organization_id || undefined}
+ options={{
+ includeSpecialOptions: true,
+ includeUserModels: !teamData?.team_info?.organization_id,
+ showAllProxyModelsOverride: isProxyAdminRole(userRole) && !teamData?.team_info?.organization_id,
+ }}
+ context="team"
+ dataTestId="models-select"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ form.setFieldValue("team_member_budget_duration", value)}
+ value={form.getFieldValue("team_member_budget_duration")}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Guardrails{" "}
+
+ e.stopPropagation()}
+ >
+
+
+
+
+ }
+ name="guardrails"
+ help="Select existing guardrails or enter new ones"
+ >
+
+
+
+ Disable Global Guardrails
+
+
+
+
+ }
+ name="disable_global_guardrails"
+ valuePropName="checked"
+ help="Bypass global guardrails for this team"
+ >
+
+
+
+
+ Policies{" "}
+
+ e.stopPropagation()}
+ >
+
+
+
+
+ }
+ name="policies"
+ help="Select existing policies or enter new ones"
+ >
+
+
+
+ form.setFieldValue("vector_stores", values)}
+ value={form.getFieldValue("vector_stores")}
+ accessToken={accessToken || ""}
+ placeholder="Select vector stores"
+ />
+
+
+
+ form.setFieldValue("allowed_passthrough_routes", values)}
+ value={form.getFieldValue("allowed_passthrough_routes")}
+ accessToken={accessToken || ""}
+ placeholder="Select pass through routes"
+ />
+
+
+
+ form.setFieldValue("mcp_servers_and_groups", val)}
+ value={form.getFieldValue("mcp_servers_and_groups")}
+ accessToken={accessToken || ""}
+ placeholder="Select MCP servers or access groups (optional)"
+ />
+
+
+ {/* Hidden field to register mcp_tool_permissions with the form */}
+
+
+
+
+
+ prevValues.mcp_servers_and_groups !== currentValues.mcp_servers_and_groups ||
+ prevValues.mcp_tool_permissions !== currentValues.mcp_tool_permissions
+ }
+ >
+ {() => (
+
+ form.setFieldsValue({ mcp_tool_permissions: toolPerms })}
+ />
+
+ )}
+
+
+
+ form.setFieldValue("agents_and_groups", val)}
+ value={form.getFieldValue("agents_and_groups")}
+ accessToken={accessToken || ""}
+ placeholder="Select agents or access groups (optional)"
+ />
+
+
+
+
+
+
+
+ form.setFieldValue("logging_settings", values)}
+ />
+
+
+ {
+ if (!value) {
+ return Promise.resolve();
+ }
+ try {
+ JSON.parse(value);
+ return Promise.resolve();
+ } catch (error) {
+ return Promise.reject(new Error("Please enter valid JSON"));
+ }
+ },
+ },
+ ]}
+ >
+
+
+
+
+
+
+
+
+
+
+ } type="primary" htmlType="submit" loading={isTeamSaving}>
+ Save Changes
+
+
+
+
+ ) : (
+
+
+
Team Name
+
{info.team_alias}
+
+
+
Team ID
+
{info.team_id}
+
+
+
Created At
+
{new Date(info.created_at).toLocaleString()}
+
+
+
Models
+
+ {info.models.map((model, index) => (
+
+ {model}
+
+ ))}
+
+
+
+
Rate Limits
+
TPM: {info.tpm_limit || "Unlimited"}
+
RPM: {info.rpm_limit || "Unlimited"}
+
+
+
Team Budget
+
+ Max Budget:{" "}
+ {info.max_budget !== null ? `$${formatNumberWithCommas(info.max_budget, 4)}` : "No Limit"}
+
+
+ Soft Budget:{" "}
+ {info.soft_budget !== null && info.soft_budget !== undefined
+ ? `$${formatNumberWithCommas(info.soft_budget, 4)}`
+ : "No Limit"}
+
+
Budget Reset: {info.budget_duration || "Never"}
+ {info.metadata?.soft_budget_alerting_emails &&
+ Array.isArray(info.metadata.soft_budget_alerting_emails) &&
+ info.metadata.soft_budget_alerting_emails.length > 0 && (
+
+ Soft Budget Alerting Emails: {info.metadata.soft_budget_alerting_emails.join(", ")}
+
+ )}
+
+
+
+ Team Member Settings{" "}
+
+
+
+
+
Max Budget: {info.team_member_budget_table?.max_budget || "No Limit"}
+
Budget Duration: {info.team_member_budget_table?.budget_duration || "No Limit"}
+
Key Duration: {info.metadata?.team_member_key_duration || "No Limit"}
+
TPM Limit: {info.team_member_budget_table?.tpm_limit || "No Limit"}
+
RPM Limit: {info.team_member_budget_table?.rpm_limit || "No Limit"}
+
+
+
Organization ID
+
{info.organization_id}
+
+
+ Status
+ {info.blocked ? "Blocked" : "Active"}
+
+
+
+
Disable Global Guardrails
+
+ {info.metadata?.disable_global_guardrails === true ? (
+ Enabled - Global guardrails bypassed
+ ) : (
+ Disabled - Global guardrails active
+ )}
+
+
+
+
+
+
+
+ {info.metadata?.secret_manager_settings && (
+
+
Secret Manager Settings
+
+ {JSON.stringify(info.metadata.secret_manager_settings, null, 2)}
+
+
+ )}
+
+ )}
+
+ ),
+ },
+ ].filter(tab => visibleTabs.includes(tab.key))}
+ />
+
+ setIsEditMemberModalVisible(false)}
+ onSubmit={handleMemberUpdate}
+ initialData={selectedEditMember}
+ mode="edit"
+ config={{
+ title: "Edit Member",
+ showEmail: true,
+ showUserId: true,
+ roleOptions: [
+ { label: "Admin", value: "admin" },
+ { label: "User", value: "user" },
+ ],
+ additionalFields: [
+ {
+ name: "max_budget_in_team",
+ label: (
+
+ Team Member Budget (USD){" "}
+
+
+
+
+ ),
+ type: "numerical" as const,
+ step: 0.01,
+ min: 0,
+ placeholder: "Budget limit for this member within this team",
+ },
+ {
+ name: "tpm_limit",
+ label: (
+
+ Team Member TPM Limit{" "}
+
+
+
+
+ ),
+ type: "numerical" as const,
+ step: 1,
+ min: 0,
+ placeholder: "Tokens per minute limit for this member in this team",
+ },
+ {
+ name: "rpm_limit",
+ label: (
+
+ Team Member RPM Limit{" "}
+
+
+
+
+ ),
+ type: "numerical" as const,
+ step: 1,
+ min: 0,
+ placeholder: "Requests per minute limit for this member in this team",
+ },
+ ],
+ }}
+ />
+
+ setIsAddMemberModalVisible(false)}
+ onSubmit={handleMemberCreate}
+ accessToken={accessToken}
+ />
+
+ {/* Delete Member Confirmation Modal */}
+
+
+ );
+};
+
+export default TeamInfoView;
diff --git a/ui/litellm-dashboard/src/components/team/TeamMemberTab.test.tsx b/ui/litellm-dashboard/src/components/team/TeamMemberTab.test.tsx
new file mode 100644
index 00000000000..d494b874e4d
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/team/TeamMemberTab.test.tsx
@@ -0,0 +1,386 @@
+import { screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { renderWithProviders } from "../../../tests/test-utils";
+import { TeamData } from "./TeamInfo";
+import TeamMembersComponent from "./TeamMemberTab";
+
+vi.mock("@/app/(dashboard)/hooks/uiSettings/useUISettings", () => ({
+ useUISettings: vi.fn(),
+}));
+
+vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
+ default: vi.fn(),
+}));
+
+vi.mock("@/utils/roles", () => ({
+ isUserTeamAdminForSingleTeam: vi.fn(() => false),
+ isProxyAdminRole: vi.fn(() => false),
+}));
+
+import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
+import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
+import { isProxyAdminRole, isUserTeamAdminForSingleTeam } from "@/utils/roles";
+
+const mockHandleMemberDelete = vi.fn();
+const mockSetSelectedEditMember = vi.fn();
+const mockSetIsEditMemberModalVisible = vi.fn();
+const mockSetIsAddMemberModalVisible = vi.fn();
+
+const createMockTeamData = (overrides: Partial = {}): TeamData => ({
+ team_id: "team-123",
+ team_info: {
+ team_alias: "Test Team",
+ team_id: "team-123",
+ organization_id: null,
+ admins: ["admin@test.com"],
+ members: ["user1@test.com"],
+ members_with_roles: [
+ {
+ user_id: "user1@test.com",
+ user_email: "user1@test.com",
+ role: "member",
+ },
+ {
+ user_id: "user2@test.com",
+ user_email: "user2@test.com",
+ role: "admin",
+ },
+ ],
+ metadata: {},
+ tpm_limit: null,
+ rpm_limit: null,
+ max_budget: null,
+ budget_duration: null,
+ models: [],
+ blocked: false,
+ spend: 0,
+ max_parallel_requests: null,
+ budget_reset_at: null,
+ model_id: null,
+ litellm_model_table: null,
+ created_at: "2024-01-01T00:00:00Z",
+ team_member_budget_table: null,
+ },
+ keys: [],
+ team_memberships: [
+ {
+ user_id: "user1@test.com",
+ team_id: "team-123",
+ budget_id: "budget1",
+ spend: 100.5,
+ litellm_budget_table: {
+ budget_id: "budget1",
+ soft_budget: null,
+ max_budget: 1000,
+ max_parallel_requests: null,
+ tpm_limit: 10000,
+ rpm_limit: 100,
+ model_max_budget: null,
+ budget_duration: null,
+ },
+ },
+ ],
+ ...overrides,
+});
+
+describe("TeamMembersComponent", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.mocked(useUISettings).mockReturnValue({
+ data: { values: { disable_team_admin_delete_team_user: false } },
+ isLoading: false,
+ isError: false,
+ error: null,
+ isSuccess: true,
+ isFetching: false,
+ refetch: vi.fn(),
+ } as any);
+
+ vi.mocked(useAuthorized).mockReturnValue({
+ isLoading: false,
+ isAuthorized: true,
+ userId: "test-user-id",
+ userRole: "Admin",
+ accessToken: "test-token",
+ token: "test-token",
+ userEmail: "test@example.com",
+ premiumUser: false,
+ disabledPersonalKeyCreation: null,
+ showSSOBanner: false,
+ });
+ });
+
+ it("should render", () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByRole("table")).toBeInTheDocument();
+ });
+
+ it("should render team members table with headers", () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByRole("columnheader", { name: /user email/i })).toBeInTheDocument();
+ expect(screen.getByRole("columnheader", { name: /user id/i })).toBeInTheDocument();
+ expect(screen.getByRole("columnheader", { name: /team role/i })).toBeInTheDocument();
+ expect(screen.getByRole("columnheader", { name: /actions/i })).toBeInTheDocument();
+ });
+
+ it("should render team members data", () => {
+ renderWithProviders(
+ ,
+ );
+
+ // user1@test.com appears twice (User ID and User Email columns)
+ expect(screen.getAllByText("user1@test.com").length).toBeGreaterThanOrEqual(1);
+ expect(screen.getAllByText("user2@test.com").length).toBeGreaterThanOrEqual(1);
+ const table = screen.getByRole("table");
+ expect(table).toHaveTextContent("member");
+ expect(table).toHaveTextContent("admin");
+ });
+
+ it("should render Add Member button", () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("Add Member")).toBeInTheDocument();
+ });
+
+ it("should display dash when user email is null", () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("-")).toBeInTheDocument();
+ });
+
+ it("should display Default Proxy Admin tag for default_user_id", () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("Default Proxy Admin")).toBeInTheDocument();
+ });
+
+ it("should display spend and rate limits for member with membership", () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText(/\$100\.5/)).toBeInTheDocument();
+ expect(screen.getByText(/100 RPM/)).toBeInTheDocument();
+ expect(screen.getByText(/10000 TPM/)).toBeInTheDocument();
+ });
+
+ it("should display No Limit for budget when member has no budget", () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("No Limit")).toBeInTheDocument();
+ });
+
+ it("should display No Limits for rate limits when member has no limits", () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("No Limits")).toBeInTheDocument();
+ });
+
+ it("should call setIsEditMemberModalVisible and setSelectedEditMember when edit button is clicked", async () => {
+ const user = userEvent.setup();
+ vi.mocked(isProxyAdminRole).mockReturnValue(true);
+ vi.mocked(isUserTeamAdminForSingleTeam).mockReturnValue(false);
+
+ renderWithProviders(
+ ,
+ );
+
+ const editButtons = screen.getAllByTestId("edit-member");
+ await user.click(editButtons[0]);
+
+ expect(mockSetIsEditMemberModalVisible).toHaveBeenCalledWith(true);
+ expect(mockSetSelectedEditMember).toHaveBeenCalled();
+ });
+
+ it("should call setIsAddMemberModalVisible when Add Member button is clicked", async () => {
+ const user = userEvent.setup();
+
+ renderWithProviders(
+ ,
+ );
+
+ const addButton = screen.getByRole("button", { name: /add member/i });
+ await user.click(addButton);
+
+ expect(mockSetIsAddMemberModalVisible).toHaveBeenCalledWith(true);
+ });
+
+ it("should hide delete button when disable_team_admin_delete_team_user is true and user is team admin", () => {
+ vi.mocked(isProxyAdminRole).mockReturnValue(false);
+ vi.mocked(isUserTeamAdminForSingleTeam).mockReturnValue(true);
+ vi.mocked(useUISettings).mockReturnValue({
+ data: { values: { disable_team_admin_delete_team_user: true } },
+ isLoading: false,
+ isError: false,
+ error: null,
+ isSuccess: true,
+ isFetching: false,
+ refetch: vi.fn(),
+ } as any);
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.queryByTestId("delete-member")).not.toBeInTheDocument();
+ expect(screen.getAllByTestId("edit-member")).toHaveLength(2);
+ });
+
+ it("should show delete button for proxy admin when canEditTeam is true", () => {
+ vi.mocked(isProxyAdminRole).mockReturnValue(true);
+ vi.mocked(isUserTeamAdminForSingleTeam).mockReturnValue(false);
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getAllByTestId("delete-member")).toHaveLength(2);
+ expect(screen.getAllByTestId("edit-member")).toHaveLength(2);
+ });
+
+ it("should hide action buttons when canEditTeam is false", () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.queryByTestId("edit-member")).not.toBeInTheDocument();
+ expect(screen.queryByTestId("delete-member")).not.toBeInTheDocument();
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/team/TeamMemberTab.tsx b/ui/litellm-dashboard/src/components/team/TeamMemberTab.tsx
new file mode 100644
index 00000000000..09a6b0f468d
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/team/TeamMemberTab.tsx
@@ -0,0 +1,233 @@
+import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
+import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
+import { Member } from "@/components/networking";
+import { formatNumberWithCommas } from "@/utils/dataUtils";
+import { isProxyAdminRole, isUserTeamAdminForSingleTeam } from "@/utils/roles";
+import { CrownOutlined, InfoCircleOutlined, UserAddOutlined, UserOutlined } from "@ant-design/icons";
+import { Button, Space, Table, Tag, Tooltip, Typography } from "antd";
+import type { ColumnsType } from "antd/es/table";
+import TableIconActionButton from "../common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
+import { TeamData } from "./TeamInfo";
+
+const { Text } = Typography;
+
+interface TeamMemberTabProps {
+ teamData: TeamData;
+ canEditTeam: boolean;
+ handleMemberDelete: (member: Member) => void;
+ setSelectedEditMember: (member: Member) => void;
+ setIsEditMemberModalVisible: (visible: boolean) => void;
+ setIsAddMemberModalVisible: (visible: boolean) => void;
+}
+
+export default function TeamMemberTab({
+ teamData,
+ canEditTeam,
+ handleMemberDelete,
+ setSelectedEditMember,
+ setIsEditMemberModalVisible,
+ setIsAddMemberModalVisible,
+}: TeamMemberTabProps) {
+ const formatNumber = (value: number | null): string => {
+ if (value === null || value === undefined) return "0";
+
+ if (typeof value === "number") {
+ // Convert scientific notation to normal decimal
+ const normalNumber = Number(value);
+
+ // If it's a whole number, return it without decimals
+ if (normalNumber === Math.floor(normalNumber)) {
+ return normalNumber.toString();
+ }
+
+ // For decimal numbers, use toFixed and remove trailing zeros
+ return formatNumberWithCommas(normalNumber, 8).replace(/\.?0+$/, "");
+ }
+
+ return "0";
+ };
+
+ // Helper function to get spend for a user
+ const getUserSpend = (userId: string | null): number | null => {
+ if (!userId) return 0;
+ const membership = teamData.team_memberships.find((tm) => tm.user_id === userId);
+ return membership?.spend || 0;
+ };
+
+ const getUserBudget = (userId: string | null): string | null => {
+ if (!userId) return null;
+ const membership = teamData.team_memberships.find((tm) => tm.user_id === userId);
+ const maxBudget = membership?.litellm_budget_table?.max_budget;
+ if (maxBudget === null || maxBudget === undefined) {
+ return null;
+ }
+ return formatNumber(maxBudget);
+ };
+
+ // Helper function to get rate limits for a user
+ const getUserRateLimits = (userId: string | null): string => {
+ if (!userId) return "No Limits";
+ const membership = teamData.team_memberships.find((tm) => tm.user_id === userId);
+ const rpmLimit = membership?.litellm_budget_table?.rpm_limit;
+ const tpmLimit = membership?.litellm_budget_table?.tpm_limit;
+
+ const rpmText = rpmLimit ? `${formatNumber(rpmLimit)} RPM` : null;
+ const tpmText = tpmLimit ? `${formatNumber(tpmLimit)} TPM` : null;
+
+ const limits = [rpmText, tpmText].filter(Boolean);
+ return limits.length > 0 ? limits.join(" / ") : "No Limits";
+ };
+
+ const { data: uiSettingsData } = useUISettings();
+ const { userId, userRole } = useAuthorized();
+ const disableTeamAdminDeleteTeamUser = Boolean(uiSettingsData?.values?.disable_team_admin_delete_team_user);
+ const isUserTeamAdmin = isUserTeamAdminForSingleTeam(teamData.team_info.members_with_roles, userId || "");
+ const isProxyAdmin = isProxyAdminRole(userRole || "");
+
+ const columns: ColumnsType = [
+ {
+ title: "User Email",
+ dataIndex: "user_email",
+ key: "user_email",
+ render: (email: string | null) => (
+ {email || "-"}
+ ),
+ },
+ {
+ title: "User ID",
+ dataIndex: "user_id",
+ key: "user_id",
+ render: (userId: string | null) =>
+ userId === "default_user_id" ? (
+ Default Proxy Admin
+ ) : (
+ {userId}
+ ),
+ },
+ {
+ title: (
+
+ Team Role
+
+
+
+
+ ),
+ dataIndex: "role",
+ key: "role",
+ render: (role: string) => (
+
+ {role?.toLowerCase() === "admin" ? (
+
+ ) : (
+
+ )}
+ {role}
+
+ ),
+ },
+ {
+ title: (
+
+ Team Member Spend (USD)
+
+
+
+
+ ),
+ key: "spend",
+ render: (_: unknown, record: Member) => (
+
+ ${formatNumberWithCommas(getUserSpend(record.user_id), 4)}
+
+ ),
+ },
+ {
+ title: "Team Member Budget (USD)",
+ key: "budget",
+ render: (_: unknown, record: Member) => {
+ const budget = getUserBudget(record.user_id);
+ return (
+
+ {budget ? `$${formatNumberWithCommas(Number(budget), 4)}` : "No Limit"}
+
+ );
+ },
+ },
+ {
+ title: (
+
+ Team Member Rate Limits
+
+
+
+
+ ),
+ key: "rate_limits",
+ render: (_: unknown, record: Member) => (
+ {getUserRateLimits(record.user_id)}
+ ),
+ },
+ {
+ title: "Actions",
+ key: "actions",
+ fixed: "right",
+ width: 120,
+ render: (_: unknown, record: Member) =>
+ canEditTeam ? (
+
+
{
+ const membership = teamData.team_memberships.find(
+ (tm) => tm.user_id === record.user_id
+ );
+ const enhancedMember = {
+ ...record,
+ max_budget_in_team:
+ membership?.litellm_budget_table?.max_budget || null,
+ tpm_limit:
+ membership?.litellm_budget_table?.tpm_limit || null,
+ rpm_limit:
+ membership?.litellm_budget_table?.rpm_limit || null,
+ };
+ setSelectedEditMember(enhancedMember);
+ setIsEditMemberModalVisible(true);
+ }}
+ />
+ {(isProxyAdmin ||
+ (isUserTeamAdmin && !disableTeamAdminDeleteTeamUser)) && (
+ handleMemberDelete(record)}
+ />
+ )}
+
+ ) : null,
+ },
+ ];
+
+ return (
+
+
record.user_id || String(index)}
+ pagination={false}
+ size="small"
+ scroll={{ x: "max-content" }}
+ />
+ }
+ type="primary"
+ onClick={() => setIsAddMemberModalVisible(true)}
+ >
+ Add Member
+
+
+ );
+};
diff --git a/ui/litellm-dashboard/src/components/team/tabVisibilityUtils.test.ts b/ui/litellm-dashboard/src/components/team/tabVisibilityUtils.test.ts
new file mode 100644
index 00000000000..5a09b4fa36c
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/team/tabVisibilityUtils.test.ts
@@ -0,0 +1,74 @@
+import { describe, expect, it } from "vitest";
+import {
+ getTeamInfoDefaultTab,
+ getTeamInfoVisibleTabs,
+ isTeamInfoTabVisible,
+ TEAM_INFO_TAB_KEYS,
+ TEAM_INFO_TAB_LABELS,
+} from "./tabVisibilityUtils";
+
+describe("team_info_tabs", () => {
+ describe("TEAM_INFO_TAB_LABELS", () => {
+ it("should have label for every tab key", () => {
+ expect(TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.OVERVIEW]).toBe("Overview");
+ expect(TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.MEMBERS]).toBe("Members");
+ expect(TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.MEMBER_PERMISSIONS]).toBe("Member Permissions");
+ expect(TEAM_INFO_TAB_LABELS[TEAM_INFO_TAB_KEYS.SETTINGS]).toBe("Settings");
+ });
+ });
+
+ describe("getTeamInfoVisibleTabs", () => {
+ it("returns only overview when user cannot edit team", () => {
+ const tabs = getTeamInfoVisibleTabs(false);
+ expect(tabs).toEqual([TEAM_INFO_TAB_KEYS.OVERVIEW]);
+ });
+
+ it("returns all tabs when user can edit team", () => {
+ const tabs = getTeamInfoVisibleTabs(true);
+ expect(tabs).toEqual([
+ TEAM_INFO_TAB_KEYS.OVERVIEW,
+ TEAM_INFO_TAB_KEYS.MEMBERS,
+ TEAM_INFO_TAB_KEYS.MEMBER_PERMISSIONS,
+ TEAM_INFO_TAB_KEYS.SETTINGS,
+ ]);
+ });
+ });
+
+ describe("getTeamInfoDefaultTab", () => {
+ it("returns overview when editTeam is false", () => {
+ expect(getTeamInfoDefaultTab(false, true)).toBe(TEAM_INFO_TAB_KEYS.OVERVIEW);
+ expect(getTeamInfoDefaultTab(false, false)).toBe(TEAM_INFO_TAB_KEYS.OVERVIEW);
+ });
+
+ it("returns settings when editTeam is true and user can edit", () => {
+ expect(getTeamInfoDefaultTab(true, true)).toBe(TEAM_INFO_TAB_KEYS.SETTINGS);
+ });
+
+ it("returns overview when editTeam is true but user cannot edit", () => {
+ expect(getTeamInfoDefaultTab(true, false)).toBe(TEAM_INFO_TAB_KEYS.OVERVIEW);
+ });
+ });
+
+ describe("isTeamInfoTabVisible", () => {
+ it("always returns true for overview tab", () => {
+ expect(isTeamInfoTabVisible(TEAM_INFO_TAB_KEYS.OVERVIEW, false)).toBe(true);
+ expect(isTeamInfoTabVisible(TEAM_INFO_TAB_KEYS.OVERVIEW, true)).toBe(true);
+ });
+
+ it("returns false for members tab when user cannot edit", () => {
+ expect(isTeamInfoTabVisible(TEAM_INFO_TAB_KEYS.MEMBERS, false)).toBe(false);
+ });
+
+ it("returns true for members tab when user can edit", () => {
+ expect(isTeamInfoTabVisible(TEAM_INFO_TAB_KEYS.MEMBERS, true)).toBe(true);
+ });
+
+ it("returns false for settings tab when user cannot edit", () => {
+ expect(isTeamInfoTabVisible(TEAM_INFO_TAB_KEYS.SETTINGS, false)).toBe(false);
+ });
+
+ it("returns true for settings tab when user can edit", () => {
+ expect(isTeamInfoTabVisible(TEAM_INFO_TAB_KEYS.SETTINGS, true)).toBe(true);
+ });
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/team/tabVisibilityUtils.ts b/ui/litellm-dashboard/src/components/team/tabVisibilityUtils.ts
new file mode 100644
index 00000000000..d77230ea09b
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/team/tabVisibilityUtils.ts
@@ -0,0 +1,59 @@
+/**
+ * Team info tab configuration and permission logic.
+ * Extracted for testability - permission rules can be unit tested in isolation.
+ */
+
+export const TEAM_INFO_TAB_KEYS = {
+ OVERVIEW: "overview",
+ MEMBERS: "members",
+ MEMBER_PERMISSIONS: "member-permissions",
+ SETTINGS: "settings",
+} as const;
+
+export const TEAM_INFO_TAB_LABELS: Record = {
+ [TEAM_INFO_TAB_KEYS.OVERVIEW]: "Overview",
+ [TEAM_INFO_TAB_KEYS.MEMBERS]: "Members",
+ [TEAM_INFO_TAB_KEYS.MEMBER_PERMISSIONS]: "Member Permissions",
+ [TEAM_INFO_TAB_KEYS.SETTINGS]: "Settings",
+};
+
+/**
+ * Returns the list of tab keys that should be visible based on permissions.
+ * - Overview: always visible
+ * - Members, Member Permissions, Settings: only when canEditTeam is true
+ */
+export function getTeamInfoVisibleTabs(canEditTeam: boolean): readonly string[] {
+ const baseTabs = [TEAM_INFO_TAB_KEYS.OVERVIEW];
+ if (canEditTeam) {
+ return [
+ ...baseTabs,
+ TEAM_INFO_TAB_KEYS.MEMBERS,
+ TEAM_INFO_TAB_KEYS.MEMBER_PERMISSIONS,
+ TEAM_INFO_TAB_KEYS.SETTINGS,
+ ];
+ }
+ return baseTabs;
+}
+
+/**
+ * Returns the default active tab key based on permissions and edit intent.
+ * - When editTeam is true and user can edit: open Settings tab
+ * - Otherwise: open Overview tab
+ */
+export function getTeamInfoDefaultTab(editTeam: boolean, canEditTeam: boolean): string {
+ if (editTeam && canEditTeam) {
+ return TEAM_INFO_TAB_KEYS.SETTINGS;
+ }
+ return TEAM_INFO_TAB_KEYS.OVERVIEW;
+}
+
+/**
+ * Checks if a specific tab should be visible based on permissions.
+ */
+export function isTeamInfoTabVisible(
+ tabKey: string,
+ canEditTeam: boolean
+): boolean {
+ const visibleTabs = getTeamInfoVisibleTabs(canEditTeam);
+ return visibleTabs.includes(tabKey);
+}
diff --git a/ui/litellm-dashboard/src/components/team/team_info.tsx b/ui/litellm-dashboard/src/components/team/team_info.tsx
deleted file mode 100644
index 014f8fb9010..00000000000
--- a/ui/litellm-dashboard/src/components/team/team_info.tsx
+++ /dev/null
@@ -1,1296 +0,0 @@
-import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
-import UserSearchModal from "@/components/common_components/user_search_modal";
-import {
- getGuardrailsList,
- getPoliciesList,
- getPolicyInfoWithGuardrails,
- Member,
- Organization,
- organizationInfoCall,
- teamInfoCall,
- teamMemberAddCall,
- teamMemberDeleteCall,
- teamMemberUpdateCall,
- teamUpdateCall,
-} from "@/components/networking";
-import { formatNumberWithCommas } from "@/utils/dataUtils";
-import { mapEmptyStringToNull } from "@/utils/keyUpdateUtils";
-import { isProxyAdminRole } from "@/utils/roles";
-import { InfoCircleOutlined } from "@ant-design/icons";
-import { ArrowLeftIcon } from "@heroicons/react/outline";
-import {
- Badge,
- Card,
- Grid,
- Tab,
- TabGroup,
- TabList,
- TabPanel,
- TabPanels,
- Text,
- TextInput,
- Title,
- Button as TremorButton,
-} from "@tremor/react";
-import { Button, Form, Input, message, Select, Switch, Tooltip } from "antd";
-import { CheckIcon, CopyIcon } from "lucide-react";
-import React, { useEffect, useMemo, useState } from "react";
-import { copyToClipboard as utilCopyToClipboard } from "../../utils/dataUtils";
-import AgentSelector from "../agent_management/AgentSelector";
-import DeleteResourceModal from "../common_components/DeleteResourceModal";
-import DurationSelect from "../common_components/DurationSelect";
-import PassThroughRoutesSelector from "../common_components/PassThroughRoutesSelector";
-import { unfurlWildcardModelsInList } from "../key_team_helpers/fetch_available_models_team_key";
-import LoggingSettingsView from "../logging_settings_view";
-import MCPServerSelector from "../mcp_server_management/MCPServerSelector";
-import MCPToolPermissions from "../mcp_server_management/MCPToolPermissions";
-import { ModelSelect } from "../ModelSelect/ModelSelect";
-import NotificationsManager from "../molecules/notifications_manager";
-import { fetchMCPAccessGroups } from "../networking";
-import ObjectPermissionsView from "../object_permissions_view";
-import NumericalInput from "../shared/numerical_input";
-import VectorStoreSelector from "../vector_store_management/VectorStoreSelector";
-import EditLoggingSettings from "./EditLoggingSettings";
-import MemberModal from "./EditMembership";
-import MemberPermissions from "./member_permissions";
-import TeamMembersComponent from "./team_member_view";
-
-export interface TeamMembership {
- user_id: string;
- team_id: string;
- budget_id: string;
- spend: number;
- litellm_budget_table: {
- budget_id: string;
- soft_budget: number | null;
- max_budget: number | null;
- max_parallel_requests: number | null;
- tpm_limit: number | null;
- rpm_limit: number | null;
- model_max_budget: Record | null;
- budget_duration: string | null;
- };
-}
-
-export interface TeamData {
- team_id: string;
- team_info: {
- team_alias: string;
- team_id: string;
- organization_id: string | null;
- admins: string[];
- members: string[];
- members_with_roles: Member[];
- metadata: Record;
- tpm_limit: number | null;
- rpm_limit: number | null;
- max_budget: number | null;
- soft_budget?: number | null;
- budget_duration: string | null;
- models: string[];
- blocked: boolean;
- spend: number;
- max_parallel_requests: number | null;
- budget_reset_at: string | null;
- model_id: string | null;
- litellm_model_table: {
- model_aliases: Record;
- } | null;
- created_at: string;
- guardrails?: string[];
- policies?: string[];
- object_permission?: {
- object_permission_id: string;
- mcp_servers: string[];
- mcp_access_groups?: string[];
- mcp_tool_permissions?: Record;
- vector_stores: string[];
- agents?: string[];
- agent_access_groups?: string[];
- };
- team_member_budget_table: {
- max_budget: number;
- budget_duration: string;
- tpm_limit: number | null;
- rpm_limit: number | null;
- } | null;
- };
- keys: any[];
- team_memberships: TeamMembership[];
-}
-
-export interface TeamInfoProps {
- teamId: string;
- onUpdate: (data: any) => void;
- onClose: () => void;
- accessToken: string | null;
- is_team_admin: boolean;
- is_proxy_admin: boolean;
- userModels: string[];
- editTeam: boolean;
- premiumUser?: boolean;
-}
-
-const getOrganizationModels = (organization: Organization | null, userModels: string[]) => {
- let tempModelsToPick = [];
-
- if (organization) {
- // Check if organization has "all-proxy-models" in its models array
- if (organization.models.includes("all-proxy-models")) {
- // Treat as all-proxy-models (use userModels)
- tempModelsToPick = userModels;
- } else if (organization.models.length > 0) {
- // Organization has specific models
- tempModelsToPick = organization.models;
- } else {
- // Empty array [] is treated as all-proxy-models
- tempModelsToPick = userModels;
- }
- } else {
- // No organization, show all available models
- tempModelsToPick = userModels;
- }
-
- return unfurlWildcardModelsInList(tempModelsToPick, userModels);
-};
-
-const TeamInfoView: React.FC = ({
- teamId,
- onClose,
- accessToken,
- is_team_admin,
- is_proxy_admin,
- userModels,
- editTeam,
- premiumUser = false,
- onUpdate,
-}) => {
- const [teamData, setTeamData] = useState(null);
- const [loading, setLoading] = useState(true);
- const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false);
- const [form] = Form.useForm();
- const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false);
- const [selectedEditMember, setSelectedEditMember] = useState(null);
- const [isEditing, setIsEditing] = useState(false);
- const [mcpAccessGroups, setMcpAccessGroups] = useState([]);
- const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false);
- const [copiedStates, setCopiedStates] = useState>({});
- const [guardrailsList, setGuardrailsList] = useState([]);
- const [policiesList, setPoliciesList] = useState([]);
- const [policyGuardrails, setPolicyGuardrails] = useState>({});
- const [loadingPolicies, setLoadingPolicies] = useState(false);
- const [memberToDelete, setMemberToDelete] = useState(null);
- const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
- const [isDeleting, setIsDeleting] = useState(false);
- const [isTeamSaving, setIsTeamSaving] = useState(false);
- const [organization, setOrganization] = useState(null);
- const { userRole } = useAuthorized();
-
- const canEditTeam = is_team_admin || is_proxy_admin;
-
- const fetchTeamInfo = async () => {
- try {
- setLoading(true);
- if (!accessToken) return;
- const response = await teamInfoCall(accessToken, teamId);
- setTeamData(response);
- } catch (error) {
- NotificationsManager.fromBackend("Failed to load team information");
- console.error("Error fetching team info:", error);
- } finally {
- setLoading(false);
- }
- };
-
- useEffect(() => {
- fetchTeamInfo();
- }, [teamId, accessToken]);
-
- // Fetch organization data when team has organization_id
- useEffect(() => {
- const fetchOrganization = async () => {
- if (!accessToken || !teamData?.team_info?.organization_id) {
- setOrganization(null);
- return;
- }
-
- try {
- const orgData = await organizationInfoCall(accessToken, teamData.team_info.organization_id);
- setOrganization(orgData);
- } catch (error) {
- console.error("Error fetching organization info:", error);
- setOrganization(null);
- }
- };
-
- fetchOrganization();
- }, [accessToken, teamData?.team_info?.organization_id]);
-
- // Compute modelsToPick based on organization and userModels
- const modelsToPick = useMemo(() => {
- return getOrganizationModels(organization, userModels);
- }, [organization, userModels]);
-
- const fetchMcpAccessGroups = async () => {
- if (!accessToken) return;
- if (mcpAccessGroupsLoaded) return;
- try {
- const groups = await fetchMCPAccessGroups(accessToken);
- setMcpAccessGroups(groups);
- setMcpAccessGroupsLoaded(true);
- } catch (error) {
- console.error("Failed to fetch MCP access groups:", error);
- }
- };
-
- useEffect(() => {
- const fetchGuardrails = async () => {
- try {
- if (!accessToken) return;
- const response = await getGuardrailsList(accessToken);
- const guardrailNames = response.guardrails.map((g: { guardrail_name: string }) => g.guardrail_name);
- setGuardrailsList(guardrailNames);
- } catch (error) {
- console.error("Failed to fetch guardrails:", error);
- }
- };
-
- const fetchPolicies = async () => {
- try {
- if (!accessToken) return;
- const response = await getPoliciesList(accessToken);
- const policyNames = response.policies.map((p: { policy_name: string }) => p.policy_name);
- setPoliciesList(policyNames);
- } catch (error) {
- console.error("Failed to fetch policies:", error);
- }
- };
-
- fetchGuardrails();
- fetchPolicies();
- }, [accessToken]);
-
- // Fetch resolved guardrails for all policies
- useEffect(() => {
- const fetchPolicyGuardrails = async () => {
- if (!accessToken || !teamData?.team_info?.policies || teamData.team_info.policies.length === 0) {
- return;
- }
-
- setLoadingPolicies(true);
- const guardrailsMap: Record = {};
-
- try {
- await Promise.all(
- teamData.team_info.policies.map(async (policyName: string) => {
- try {
- const policyInfo = await getPolicyInfoWithGuardrails(accessToken, policyName);
- guardrailsMap[policyName] = policyInfo.resolved_guardrails || [];
- } catch (error) {
- console.error(`Failed to fetch guardrails for policy ${policyName}:`, error);
- guardrailsMap[policyName] = [];
- }
- })
- );
- setPolicyGuardrails(guardrailsMap);
- } catch (error) {
- console.error("Failed to fetch policy guardrails:", error);
- } finally {
- setLoadingPolicies(false);
- }
- };
-
- fetchPolicyGuardrails();
- }, [accessToken, teamData?.team_info?.policies]);
-
- const handleMemberCreate = async (values: any) => {
- try {
- if (accessToken == null) return;
-
- const member: Member = {
- user_email: values.user_email,
- user_id: values.user_id,
- role: values.role,
- };
-
- await teamMemberAddCall(accessToken, teamId, member);
-
- NotificationsManager.success("Team member added successfully");
- setIsAddMemberModalVisible(false);
- form.resetFields();
-
- // Fetch updated team info
- const updatedTeamData = await teamInfoCall(accessToken, teamId);
- setTeamData(updatedTeamData);
-
- // Notify parent component of the update
- onUpdate(updatedTeamData);
- } catch (error: any) {
- let errMsg = "Failed to add team member";
-
- if (error?.raw?.detail?.error?.includes("Assigning team admins is a premium feature")) {
- errMsg = "Assigning admins is an enterprise-only feature. Please upgrade your LiteLLM plan to enable this.";
- } else if (error?.message) {
- errMsg = error.message;
- }
-
- NotificationsManager.fromBackend(errMsg);
- console.error("Error adding team member:", error);
- }
- };
-
- const handleMemberUpdate = async (values: any) => {
- try {
- if (accessToken == null) {
- return;
- }
-
- const member: Member = {
- user_email: values.user_email,
- user_id: values.user_id,
- role: values.role,
- max_budget_in_team: values.max_budget_in_team,
- tpm_limit: values.tpm_limit,
- rpm_limit: values.rpm_limit,
- };
- console.log("Updating member with values:", member);
- message.destroy(); // Remove all existing toasts
-
- await teamMemberUpdateCall(accessToken, teamId, member);
-
- NotificationsManager.success("Team member updated successfully");
- setIsEditMemberModalVisible(false);
-
- // Fetch updated team info
- const updatedTeamData = await teamInfoCall(accessToken, teamId);
- setTeamData(updatedTeamData);
-
- // Notify parent component of the update
- onUpdate(updatedTeamData);
- } catch (error: any) {
- let errMsg = "Failed to update team member";
- if (error?.raw?.detail?.includes("Assigning team admins is a premium feature")) {
- errMsg = "Assigning admins is an enterprise-only feature. Please upgrade your LiteLLM plan to enable this.";
- } else if (error?.message) {
- errMsg = error.message;
- }
- setIsEditMemberModalVisible(false);
-
- message.destroy(); // Remove all existing toasts
-
- NotificationsManager.fromBackend(errMsg);
- console.error("Error updating team member:", error);
- }
- };
-
- const handleMemberDelete = (member: Member) => {
- setMemberToDelete(member);
- setIsDeleteModalOpen(true);
- };
-
- const handleDeleteConfirm = async () => {
- if (!memberToDelete || !accessToken) return;
-
- setIsDeleting(true);
- try {
- await teamMemberDeleteCall(accessToken, teamId, memberToDelete);
-
- NotificationsManager.success("Team member removed successfully");
-
- // Fetch updated team info
- const updatedTeamData = await teamInfoCall(accessToken, teamId);
- setTeamData(updatedTeamData);
-
- // Notify parent component of the update
- onUpdate(updatedTeamData);
- } catch (error) {
- NotificationsManager.fromBackend("Failed to remove team member");
- console.error("Error removing team member:", error);
- } finally {
- setIsDeleting(false);
- setIsDeleteModalOpen(false);
- setMemberToDelete(null);
- }
- };
-
- const handleDeleteCancel = () => {
- setIsDeleteModalOpen(false);
- setMemberToDelete(null);
- };
-
- const handleTeamUpdate = async (values: any) => {
- try {
- if (!accessToken) return;
- setIsTeamSaving(true);
-
- let parsedMetadata = {};
- try {
- const rawMetadata = values.metadata ? JSON.parse(values.metadata) : {};
- // Exclude soft_budget_alerting_emails from parsed metadata since it's handled separately
- const { soft_budget_alerting_emails, ...rest } = rawMetadata;
- parsedMetadata = rest;
- } catch (e) {
- NotificationsManager.fromBackend("Invalid JSON in metadata field");
- return;
- }
-
- let secretManagerSettings: Record | undefined;
- if (typeof values.secret_manager_settings === "string") {
- const trimmedSecretConfig = values.secret_manager_settings.trim();
- if (trimmedSecretConfig.length > 0) {
- try {
- secretManagerSettings = JSON.parse(values.secret_manager_settings);
- } catch (e) {
- NotificationsManager.fromBackend("Invalid JSON in secret manager settings");
- return;
- }
- }
- }
-
- const sanitizeNumeric = (v: any) => {
- if (v === null || v === undefined) return null;
- if (typeof v === "string" && v.trim() === "") return null;
- if (typeof v === "number" && Number.isNaN(v)) return null;
- return v;
- };
-
- const updateData: any = {
- team_id: teamId,
- team_alias: values.team_alias,
- models: values.models,
- tpm_limit: sanitizeNumeric(values.tpm_limit),
- rpm_limit: sanitizeNumeric(values.rpm_limit),
- max_budget: values.max_budget,
- soft_budget: sanitizeNumeric(values.soft_budget),
- budget_duration: values.budget_duration,
- metadata: {
- ...parsedMetadata,
- ...(values.guardrails?.length > 0 ? { guardrails: values.guardrails } : {}),
- ...(values.logging_settings?.length > 0 ? { logging: values.logging_settings } : {}),
- disable_global_guardrails: values.disable_global_guardrails || false,
- soft_budget_alerting_emails:
- typeof values.soft_budget_alerting_emails === "string"
- ? values.soft_budget_alerting_emails
- .split(",")
- .map((email: string) => email.trim())
- .filter((email: string) => email.length > 0)
- : values.soft_budget_alerting_emails || [],
- ...(secretManagerSettings !== undefined ? { secret_manager_settings: secretManagerSettings } : {}),
- },
- ...(values.policies?.length > 0 ? { policies: values.policies } : {}),
- organization_id: values.organization_id,
- };
-
- updateData.max_budget = mapEmptyStringToNull(updateData.max_budget);
- updateData.team_member_budget_duration = values.team_member_budget_duration;
-
- if (values.team_member_budget !== undefined) {
- updateData.team_member_budget = Number(values.team_member_budget);
- }
-
- if (values.team_member_key_duration !== undefined) {
- updateData.team_member_key_duration = values.team_member_key_duration;
- }
-
- if (values.team_member_tpm_limit !== undefined || values.team_member_rpm_limit !== undefined) {
- updateData.team_member_tpm_limit = sanitizeNumeric(values.team_member_tpm_limit);
- updateData.team_member_rpm_limit = sanitizeNumeric(values.team_member_rpm_limit);
- }
-
- // Handle object_permission updates
- const { servers, accessGroups } = values.mcp_servers_and_groups || {
- servers: [],
- accessGroups: [],
- };
- const serverIds = new Set(servers || []);
- const mcpToolPermissions = Object.fromEntries(
- Object.entries(values.mcp_tool_permissions || {}).filter(([serverId]) => serverIds.has(serverId)),
- );
-
- updateData.object_permission = {};
- if (servers) {
- updateData.object_permission.mcp_servers = servers;
- }
- if (accessGroups) {
- updateData.object_permission.mcp_access_groups = accessGroups;
- }
- if (mcpToolPermissions) {
- updateData.object_permission.mcp_tool_permissions = mcpToolPermissions;
- }
- delete values.mcp_servers_and_groups;
- delete values.mcp_tool_permissions;
-
- // Handle agent permissions
- const { agents, accessGroups: agentAccessGroups } = values.agents_and_groups || {
- agents: [],
- accessGroups: [],
- };
- if (agents && agents.length > 0) {
- updateData.object_permission.agents = agents;
- }
- if (agentAccessGroups && agentAccessGroups.length > 0) {
- updateData.object_permission.agent_access_groups = agentAccessGroups;
- }
- delete values.agents_and_groups;
-
- // Handle vector stores permissions
- if (values.vector_stores && values.vector_stores.length > 0) {
- updateData.object_permission.vector_stores = values.vector_stores;
- }
-
- const response = await teamUpdateCall(accessToken, updateData);
-
- NotificationsManager.success("Team settings updated successfully");
- setIsEditing(false);
- fetchTeamInfo();
- } catch (error) {
- console.error("Error updating team:", error);
- } finally {
- setIsTeamSaving(false);
- }
- };
-
- if (loading) {
- return Loading...
;
- }
-
- if (!teamData?.team_info) {
- return Team not found
;
- }
-
- const { team_info: info } = teamData;
-
- const copyToClipboard = async (text: string, key: string) => {
- const success = await utilCopyToClipboard(text);
- if (success) {
- setCopiedStates((prev) => ({ ...prev, [key]: true }));
- setTimeout(() => {
- setCopiedStates((prev) => ({ ...prev, [key]: false }));
- }, 2000);
- }
- };
-
- return (
-
-
-
-
- Back to Teams
-
-
{info.team_alias}
-
- {info.team_id}
- : }
- onClick={() => copyToClipboard(info.team_id, "team-id")}
- className={`left-2 z-10 transition-all duration-200 ${copiedStates["team-id"]
- ? "text-green-600 bg-green-50 border-green-200"
- : "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
- }`}
- />
-
-
-
-
-
-
- {[
- Overview,
- ...(canEditTeam
- ? [
- Members,
- Member Permissions,
- Settings,
- ]
- : []),
- ]}
-
-
-
- {/* Overview Panel */}
-
-
-
- Budget Status
-
-
${formatNumberWithCommas(info.spend, 4)}
-
- of {info.max_budget === null ? "Unlimited" : `$${formatNumberWithCommas(info.max_budget, 4)}`}
-
- {info.budget_duration && Reset: {info.budget_duration}}
-
- {info.team_member_budget_table && (
-
- Team Member Budget: ${formatNumberWithCommas(info.team_member_budget_table.max_budget, 4)}
-
- )}
-
-
-
-
- Rate Limits
-
- TPM: {info.tpm_limit || "Unlimited"}
- RPM: {info.rpm_limit || "Unlimited"}
- {info.max_parallel_requests && Max Parallel Requests: {info.max_parallel_requests}}
-
-
-
-
- Models
-
- {info.models.length === 0 ? (
- All proxy models
- ) : (
- info.models.map((model, index) => (
-
- {model}
-
- ))
- )}
-
-
-
-
- Virtual Keys
-
- User Keys: {teamData.keys.filter((key) => key.user_id).length}
- Service Account Keys: {teamData.keys.filter((key) => !key.user_id).length}
- Total: {teamData.keys.length}
-
-
-
-
-
-
- Guardrails
- {info.guardrails && info.guardrails.length > 0 ? (
-
- {info.guardrails.map((guardrail: string, index: number) => (
-
- {guardrail}
-
- ))}
-
- ) : (
- No guardrails configured
- )}
- {info.metadata?.disable_global_guardrails && (
-
- Global Guardrails Disabled
-
- )}
-
-
-
- Policies
- {info.policies && info.policies.length > 0 ? (
-
- {info.policies.map((policy: string, index: number) => (
-
-
- {policy}
- {loadingPolicies && Loading guardrails...}
-
- {!loadingPolicies && policyGuardrails[policy] && policyGuardrails[policy].length > 0 && (
-
-
Resolved Guardrails:
-
- {policyGuardrails[policy].map((guardrail: string, gIndex: number) => (
-
- {guardrail}
-
- ))}
-
-
- )}
-
- ))}
-
- ) : (
- No policies configured
- )}
-
-
-
-
-
-
- {/* Members Panel */}
-
-
-
-
- {/* Member Permissions Panel */}
- {canEditTeam && (
-
-
-
- )}
-
- {/* Settings Panel */}
-
-
-
-
Team Settings
- {canEditTeam && !isEditing && (
- setIsEditing(true)}>Edit Settings
- )}
-
-
- {isEditing ? (
-
-
-
-
-
- form.setFieldValue("models", values)}
- teamID={teamId}
- organizationID={teamData?.team_info?.organization_id || undefined}
- options={{
- includeSpecialOptions: true,
- includeUserModels: !teamData?.team_info?.organization_id,
- showAllProxyModelsOverride: isProxyAdminRole(userRole) && !teamData?.team_info?.organization_id,
- }}
- context="team"
- dataTestId="models-select"
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- form.setFieldValue("team_member_budget_duration", value)}
- value={form.getFieldValue("team_member_budget_duration")}
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Guardrails{" "}
-
- e.stopPropagation()}
- >
-
-
-
-
- }
- name="guardrails"
- help="Select existing guardrails or enter new ones"
- >
-
-
-
- Disable Global Guardrails
-
-
-
-
- }
- name="disable_global_guardrails"
- valuePropName="checked"
- help="Bypass global guardrails for this team"
- >
-
-
-
-
- Policies{" "}
-
- e.stopPropagation()}
- >
-
-
-
-
- }
- name="policies"
- help="Select existing policies or enter new ones"
- >
-
-
-
- form.setFieldValue("vector_stores", values)}
- value={form.getFieldValue("vector_stores")}
- accessToken={accessToken || ""}
- placeholder="Select vector stores"
- />
-
-
-
- form.setFieldValue("allowed_passthrough_routes", values)}
- value={form.getFieldValue("allowed_passthrough_routes")}
- accessToken={accessToken || ""}
- placeholder="Select pass through routes"
- />
-
-
-
- form.setFieldValue("mcp_servers_and_groups", val)}
- value={form.getFieldValue("mcp_servers_and_groups")}
- accessToken={accessToken || ""}
- placeholder="Select MCP servers or access groups (optional)"
- />
-
-
- {/* Hidden field to register mcp_tool_permissions with the form */}
-
-
-
-
-
- prevValues.mcp_servers_and_groups !== currentValues.mcp_servers_and_groups ||
- prevValues.mcp_tool_permissions !== currentValues.mcp_tool_permissions
- }
- >
- {() => (
-
- form.setFieldsValue({ mcp_tool_permissions: toolPerms })}
- />
-
- )}
-
-
-
- form.setFieldValue("agents_and_groups", val)}
- value={form.getFieldValue("agents_and_groups")}
- accessToken={accessToken || ""}
- placeholder="Select agents or access groups (optional)"
- />
-
-
-
-
-
-
-
- form.setFieldValue("logging_settings", values)}
- />
-
-
- {
- if (!value) {
- return Promise.resolve();
- }
- try {
- JSON.parse(value);
- return Promise.resolve();
- } catch (error) {
- return Promise.reject(new Error("Please enter valid JSON"));
- }
- },
- },
- ]}
- >
-
-
-
-
-
-
-
-
-
- setIsEditing(false)} disabled={isTeamSaving}>
- Cancel
-
-
- Save Changes
-
-
-
-
- ) : (
-
-
-
Team Name
-
{info.team_alias}
-
-
-
Team ID
-
{info.team_id}
-
-
-
Created At
-
{new Date(info.created_at).toLocaleString()}
-
-
-
Models
-
- {info.models.map((model, index) => (
-
- {model}
-
- ))}
-
-
-
-
Rate Limits
-
TPM: {info.tpm_limit || "Unlimited"}
-
RPM: {info.rpm_limit || "Unlimited"}
-
-
-
Team Budget
-
- Max Budget:{" "}
- {info.max_budget !== null ? `$${formatNumberWithCommas(info.max_budget, 4)}` : "No Limit"}
-
-
- Soft Budget:{" "}
- {info.soft_budget !== null && info.soft_budget !== undefined
- ? `$${formatNumberWithCommas(info.soft_budget, 4)}`
- : "No Limit"}
-
-
Budget Reset: {info.budget_duration || "Never"}
- {info.metadata?.soft_budget_alerting_emails &&
- Array.isArray(info.metadata.soft_budget_alerting_emails) &&
- info.metadata.soft_budget_alerting_emails.length > 0 && (
-
- Soft Budget Alerting Emails: {info.metadata.soft_budget_alerting_emails.join(", ")}
-
- )}
-
-
-
- Team Member Settings{" "}
-
-
-
-
-
Max Budget: {info.team_member_budget_table?.max_budget || "No Limit"}
-
Budget Duration: {info.team_member_budget_table?.budget_duration || "No Limit"}
-
Key Duration: {info.metadata?.team_member_key_duration || "No Limit"}
-
TPM Limit: {info.team_member_budget_table?.tpm_limit || "No Limit"}
-
RPM Limit: {info.team_member_budget_table?.rpm_limit || "No Limit"}
-
-
-
Organization ID
-
{info.organization_id}
-
-
- Status
- {info.blocked ? "Blocked" : "Active"}
-
-
-
-
Disable Global Guardrails
-
- {info.metadata?.disable_global_guardrails === true ? (
- Enabled - Global guardrails bypassed
- ) : (
- Disabled - Global guardrails active
- )}
-
-
-
-
-
-
-
- {info.metadata?.secret_manager_settings && (
-
-
Secret Manager Settings
-
- {JSON.stringify(info.metadata.secret_manager_settings, null, 2)}
-
-
- )}
-
- )}
-
-
-
-
-
-
setIsEditMemberModalVisible(false)}
- onSubmit={handleMemberUpdate}
- initialData={selectedEditMember}
- mode="edit"
- config={{
- title: "Edit Member",
- showEmail: true,
- showUserId: true,
- roleOptions: [
- { label: "Admin", value: "admin" },
- { label: "User", value: "user" },
- ],
- additionalFields: [
- {
- name: "max_budget_in_team",
- label: (
-
- Team Member Budget (USD){" "}
-
-
-
-
- ),
- type: "numerical" as const,
- step: 0.01,
- min: 0,
- placeholder: "Budget limit for this member within this team",
- },
- {
- name: "tpm_limit",
- label: (
-
- Team Member TPM Limit{" "}
-
-
-
-
- ),
- type: "numerical" as const,
- step: 1,
- min: 0,
- placeholder: "Tokens per minute limit for this member in this team",
- },
- {
- name: "rpm_limit",
- label: (
-
- Team Member RPM Limit{" "}
-
-
-
-
- ),
- type: "numerical" as const,
- step: 1,
- min: 0,
- placeholder: "Requests per minute limit for this member in this team",
- },
- ],
- }}
- />
-
- setIsAddMemberModalVisible(false)}
- onSubmit={handleMemberCreate}
- accessToken={accessToken}
- />
-
- {/* Delete Member Confirmation Modal */}
-
-
- );
-};
-
-export default TeamInfoView;
diff --git a/ui/litellm-dashboard/src/components/team/team_member_view.test.tsx b/ui/litellm-dashboard/src/components/team/team_member_view.test.tsx
deleted file mode 100644
index 30a06179c2f..00000000000
--- a/ui/litellm-dashboard/src/components/team/team_member_view.test.tsx
+++ /dev/null
@@ -1,193 +0,0 @@
-import { screen } from "@testing-library/react";
-import { beforeEach, describe, expect, it, vi } from "vitest";
-import { renderWithProviders } from "../../../tests/test-utils";
-import { TeamData } from "./team_info";
-import TeamMembersComponent from "./team_member_view";
-
-// Mock the hooks
-vi.mock("@/app/(dashboard)/hooks/uiSettings/useUISettings", () => ({
- useUISettings: vi.fn(),
-}));
-
-vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
- default: vi.fn(),
-}));
-
-vi.mock("@/utils/roles", () => ({
- isUserTeamAdminForSingleTeam: vi.fn(() => false),
- isProxyAdminRole: vi.fn(() => false),
-}));
-
-import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
-import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
-import { isProxyAdminRole, isUserTeamAdminForSingleTeam } from "@/utils/roles";
-
-describe("TeamMembersComponent", () => {
- const mockHandleMemberDelete = vi.fn();
- const mockSetSelectedEditMember = vi.fn();
- const mockSetIsEditMemberModalVisible = vi.fn();
- const mockSetIsAddMemberModalVisible = vi.fn();
-
- const mockTeamData: TeamData = {
- team_id: "team-123",
- team_info: {
- team_alias: "Test Team",
- team_id: "team-123",
- organization_id: null,
- admins: ["admin@test.com"],
- members: ["user1@test.com"],
- members_with_roles: [
- {
- user_id: "user1@test.com",
- user_email: "user1@test.com",
- role: "member",
- },
- {
- user_id: "user2@test.com",
- user_email: "user2@test.com",
- role: "admin",
- },
- ],
- metadata: {},
- tpm_limit: null,
- rpm_limit: null,
- max_budget: null,
- budget_duration: null,
- models: [],
- blocked: false,
- spend: 0,
- max_parallel_requests: null,
- budget_reset_at: null,
- model_id: null,
- litellm_model_table: null,
- created_at: "2024-01-01T00:00:00Z",
- team_member_budget_table: null,
- },
- keys: [],
- team_memberships: [
- {
- user_id: "user1@test.com",
- team_id: "team-123",
- budget_id: "budget1",
- spend: 100.5,
- litellm_budget_table: {
- budget_id: "budget1",
- soft_budget: null,
- max_budget: 1000,
- max_parallel_requests: null,
- tpm_limit: 10000,
- rpm_limit: 100,
- model_max_budget: null,
- budget_duration: null,
- },
- },
- ],
- };
-
- beforeEach(() => {
- vi.clearAllMocks();
- vi.mocked(useUISettings).mockReturnValue({
- data: { values: { disable_team_admin_delete_team_user: false } },
- isLoading: false,
- isError: false,
- error: null,
- isSuccess: true,
- isFetching: false,
- refetch: vi.fn(),
- } as any);
-
- vi.mocked(useAuthorized).mockReturnValue({
- userId: "test-user-id",
- userRole: "Admin",
- accessToken: "test-token",
- token: "test-token",
- userEmail: "test@example.com",
- premiumUser: false,
- disabledPersonalKeyCreation: null,
- showSSOBanner: false,
- });
- });
-
- it("should render team members table with headers", () => {
- renderWithProviders(
- ,
- );
-
- expect(screen.getByText("User ID")).toBeInTheDocument();
- expect(screen.getByText("User Email")).toBeInTheDocument();
- expect(screen.getByText("Role")).toBeInTheDocument();
- expect(screen.getByText("Team Member Spend (USD)")).toBeInTheDocument();
- expect(screen.getByText("Team Member Budget (USD)")).toBeInTheDocument();
- expect(screen.getByText("Team Member Rate Limits")).toBeInTheDocument();
- expect(screen.getByText("Actions")).toBeInTheDocument();
- });
-
- it("should render team members data", () => {
- renderWithProviders(
- ,
- );
-
- // user1@test.com appears twice (User ID and User Email columns)
- expect(screen.getAllByText("user1@test.com")).toHaveLength(2);
- // user2@test.com appears twice (User ID and User Email columns)
- expect(screen.getAllByText("user2@test.com")).toHaveLength(2);
- expect(screen.getByText("member")).toBeInTheDocument();
- expect(screen.getByText("admin")).toBeInTheDocument();
- });
-
- it("should render Add Member button", () => {
- renderWithProviders(
- ,
- );
-
- expect(screen.getByText("Add Member")).toBeInTheDocument();
- });
-
- it("should show delete button for proxy admin when canEditTeam is true", () => {
- vi.mocked(isProxyAdminRole).mockReturnValue(true);
- vi.mocked(isUserTeamAdminForSingleTeam).mockReturnValue(false);
-
- const { container } = renderWithProviders(
- ,
- );
-
- // Verify that action buttons are rendered when canEditTeam is true
- // For proxy admin, both edit and delete buttons should be visible
- // Check for clickable icon elements (Tremor Icon components with cursor-pointer class)
- const clickableIcons = container.querySelectorAll('[class*="cursor-pointer"]');
- // Should have at least 4 icons: 2 edit buttons + 2 delete buttons for 2 members
- expect(clickableIcons.length).toBeGreaterThanOrEqual(4);
-
- // Verify members are rendered
- expect(screen.getAllByText("user1@test.com").length).toBeGreaterThan(0);
- expect(screen.getAllByText("user2@test.com").length).toBeGreaterThan(0);
- });
-});
diff --git a/ui/litellm-dashboard/src/components/team/team_member_view.tsx b/ui/litellm-dashboard/src/components/team/team_member_view.tsx
deleted file mode 100644
index 10b3cbd83e6..00000000000
--- a/ui/litellm-dashboard/src/components/team/team_member_view.tsx
+++ /dev/null
@@ -1,193 +0,0 @@
-import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
-import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
-import { Member } from "@/components/networking";
-import { formatNumberWithCommas } from "@/utils/dataUtils";
-import { isProxyAdminRole, isUserTeamAdminForSingleTeam } from "@/utils/roles";
-import { InfoCircleOutlined } from "@ant-design/icons";
-import {
- Card,
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeaderCell,
- TableRow,
- Text,
- Button as TremorButton,
-} from "@tremor/react";
-import { Tooltip } from "antd";
-import React from "react";
-import TableIconActionButton from "../common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
-import { TeamData } from "./team_info";
-
-interface TeamMembersComponentProps {
- teamData: TeamData;
- canEditTeam: boolean;
- handleMemberDelete: (member: Member) => void;
- setSelectedEditMember: (member: Member) => void;
- setIsEditMemberModalVisible: (visible: boolean) => void;
- setIsAddMemberModalVisible: (visible: boolean) => void;
-}
-
-const TeamMembersComponent: React.FC = ({
- teamData,
- canEditTeam,
- handleMemberDelete,
- setSelectedEditMember,
- setIsEditMemberModalVisible,
- setIsAddMemberModalVisible,
-}) => {
- console.log("Team data", teamData);
- // Helper function to convert scientific notation to normal decimal format
- const formatNumber = (value: number | null): string => {
- if (value === null || value === undefined) return "0";
-
- if (typeof value === "number") {
- // Convert scientific notation to normal decimal
- const normalNumber = Number(value);
-
- // If it's a whole number, return it without decimals
- if (normalNumber === Math.floor(normalNumber)) {
- return normalNumber.toString();
- }
-
- // For decimal numbers, use toFixed and remove trailing zeros
- return formatNumberWithCommas(normalNumber, 8).replace(/\.?0+$/, "");
- }
-
- return "0";
- };
-
- // Helper function to get spend for a user
- const getUserSpend = (userId: string | null): number | null => {
- if (!userId) return 0;
- const membership = teamData.team_memberships.find((tm) => tm.user_id === userId);
- return membership?.spend || 0;
- };
-
- const getUserBudget = (userId: string | null): string | null => {
- if (!userId) return null;
- const membership = teamData.team_memberships.find((tm) => tm.user_id === userId);
- console.log(`membership=${membership}`);
- const maxBudget = membership?.litellm_budget_table?.max_budget;
- if (maxBudget === null || maxBudget === undefined) {
- return null;
- }
- return formatNumber(maxBudget);
- };
-
- // Helper function to get rate limits for a user
- const getUserRateLimits = (userId: string | null): string => {
- if (!userId) return "No Limits";
- const membership = teamData.team_memberships.find((tm) => tm.user_id === userId);
- const rpmLimit = membership?.litellm_budget_table?.rpm_limit;
- const tpmLimit = membership?.litellm_budget_table?.tpm_limit;
-
- const rpmText = rpmLimit ? `${formatNumber(rpmLimit)} RPM` : null;
- const tpmText = tpmLimit ? `${formatNumber(tpmLimit)} TPM` : null;
-
- const limits = [rpmText, tpmText].filter(Boolean);
- return limits.length > 0 ? limits.join(" / ") : "No Limits";
- };
-
- const { data: uiSettingsData } = useUISettings();
- const { userId, userRole } = useAuthorized();
- const disableTeamAdminDeleteTeamUser = Boolean(uiSettingsData?.values?.disable_team_admin_delete_team_user);
- const isUserTeamAdmin = isUserTeamAdminForSingleTeam(teamData.team_info.members_with_roles, userId || "");
- const isProxyAdmin = isProxyAdminRole(userRole || "");
-
- return (
-
-
-
-
-
-
- User ID
- User Email
- Role
-
- Team Member Spend (USD){" "}
-
-
-
-
- Team Member Budget (USD)
-
- Team Member Rate Limits{" "}
-
-
-
-
-
- Actions
-
-
-
-
-
- {teamData.team_info.members_with_roles.map((member: Member, index: number) => (
-
-
- {member.user_id}
-
-
- {member.user_email ? member.user_email : "No Email"}
-
-
- {member.role}
-
-
- ${formatNumberWithCommas(getUserSpend(member.user_id), 4)}
-
-
-
- {getUserBudget(member.user_id)
- ? `$${formatNumberWithCommas(Number(getUserBudget(member.user_id)), 4)}`
- : "No Limit"}
-
-
-
- {getUserRateLimits(member.user_id)}
-
-
- {canEditTeam && (
-
-
{
- // Get budget and rate limit data from team membership
- const membership = teamData.team_memberships.find((tm) => tm.user_id === member.user_id);
- const enhancedMember = {
- ...member,
- max_budget_in_team: membership?.litellm_budget_table?.max_budget || null,
- tpm_limit: membership?.litellm_budget_table?.tpm_limit || null,
- rpm_limit: membership?.litellm_budget_table?.rpm_limit || null,
- };
- setSelectedEditMember(enhancedMember);
- setIsEditMemberModalVisible(true);
- }}
- />
- {(isProxyAdmin || (isUserTeamAdmin && !disableTeamAdminDeleteTeamUser)) && (
- handleMemberDelete(member)}
- />
- )}
-
- )}
-
-
- ))}
-
-
-
-
-
setIsAddMemberModalVisible(true)}>Add Member
-
- );
-};
-
-export default TeamMembersComponent;