mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-07 00:24:03 +00:00
This reverts commit 65bda75f31.
This commit is contained in:
@@ -111,3 +111,7 @@ litellm_settings:
|
||||
# supported_call_types: ["acompletion", "completion"]
|
||||
callbacks: ["prometheus", "langfuse"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+1
-12
@@ -893,12 +893,6 @@ class LiteLLM_MCPServerTable(LiteLLMPydanticObjectBase):
|
||||
updated_by: Optional[str] = None
|
||||
|
||||
|
||||
class NewUserRequestTeam(LiteLLMPydanticObjectBase):
|
||||
team_id: str
|
||||
max_budget_in_team: Optional[float] = None
|
||||
user_role: Literal["user", "admin"] = "user"
|
||||
|
||||
|
||||
class NewUserRequest(GenerateRequestBase):
|
||||
max_budget: Optional[float] = None
|
||||
user_email: Optional[str] = None
|
||||
@@ -911,7 +905,7 @@ class NewUserRequest(GenerateRequestBase):
|
||||
LitellmUserRoles.INTERNAL_USER_VIEW_ONLY,
|
||||
]
|
||||
] = None
|
||||
teams: Optional[Union[List[str], List[NewUserRequestTeam]]] = None
|
||||
teams: Optional[list] = None
|
||||
auto_create_key: bool = (
|
||||
True # flag used for returning a key as part of the /user/new response
|
||||
)
|
||||
@@ -3037,11 +3031,6 @@ class DefaultInternalUserParams(LiteLLMPydanticObjectBase):
|
||||
default=None, description="Default list of models that new users can access"
|
||||
)
|
||||
|
||||
teams: Optional[Union[List[str], List[NewUserRequestTeam]]] = Field(
|
||||
default=None,
|
||||
description="Default teams for new users created",
|
||||
)
|
||||
|
||||
|
||||
class BaseDailySpendTransaction(TypedDict):
|
||||
date: str
|
||||
|
||||
@@ -55,13 +55,11 @@ router = APIRouter()
|
||||
def _update_internal_new_user_params(data_json: dict, data: NewUserRequest) -> dict:
|
||||
if "user_id" in data_json and data_json["user_id"] is None:
|
||||
data_json["user_id"] = str(uuid.uuid4())
|
||||
|
||||
auto_create_key = data_json.pop("auto_create_key", True)
|
||||
|
||||
if auto_create_key is False:
|
||||
data_json["table_name"] = (
|
||||
"user" # only create a user, don't create key if 'auto_create_key' set to False
|
||||
)
|
||||
data_json[
|
||||
"table_name"
|
||||
] = "user" # only create a user, don't create key if 'auto_create_key' set to False
|
||||
|
||||
if litellm.default_internal_user_params:
|
||||
for key, value in litellm.default_internal_user_params.items():
|
||||
@@ -93,7 +91,6 @@ def _update_internal_new_user_params(data_json: dict, data: NewUserRequest) -> d
|
||||
):
|
||||
data_json["budget_duration"] = litellm.internal_user_budget_duration
|
||||
|
||||
data_json.pop("teams", None) # handled separately
|
||||
return data_json
|
||||
|
||||
|
||||
@@ -161,85 +158,6 @@ async def _add_user_to_organizations(
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
|
||||
async def _add_user_to_team(
|
||||
user_id: str,
|
||||
team_id: str,
|
||||
user_api_key_dict: UserAPIKeyAuth,
|
||||
user_email: Optional[str] = None,
|
||||
max_budget_in_team: Optional[float] = None,
|
||||
user_role: Literal["user", "admin"] = "user",
|
||||
):
|
||||
from litellm.proxy.management_endpoints.team_endpoints import team_member_add
|
||||
|
||||
try:
|
||||
await team_member_add(
|
||||
data=TeamMemberAddRequest(
|
||||
team_id=team_id,
|
||||
member=Member(
|
||||
user_id=user_id,
|
||||
role=user_role,
|
||||
user_email=user_email,
|
||||
),
|
||||
max_budget_in_team=max_budget_in_team,
|
||||
),
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
except HTTPException as e:
|
||||
if e.status_code == 400 and (
|
||||
"already exists" in str(e) or "doesn't exist" in str(e)
|
||||
):
|
||||
verbose_proxy_logger.debug(
|
||||
"litellm.proxy.management_endpoints.internal_user_endpoints.new_user(): User already exists in team - {}".format(
|
||||
str(e)
|
||||
)
|
||||
)
|
||||
else:
|
||||
verbose_proxy_logger.debug(
|
||||
"litellm.proxy.management_endpoints.internal_user_endpoints.new_user(): Exception occured - {}".format(
|
||||
str(e)
|
||||
)
|
||||
)
|
||||
except ProxyException as e:
|
||||
if "already exists" in str(e) or "doesn't exist" in str(e):
|
||||
verbose_proxy_logger.debug(
|
||||
"litellm.proxy.management_endpoints.internal_user_endpoints.new_user(): User already exists in team - {}".format(
|
||||
str(e)
|
||||
)
|
||||
)
|
||||
elif ProxyErrorTypes.team_member_already_in_team in e.type:
|
||||
verbose_proxy_logger.debug(
|
||||
"litellm.proxy.management_endpoints.internal_user_endpoints.new_user(): User already exists in team - {}".format(
|
||||
str(e)
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise e
|
||||
|
||||
|
||||
def check_if_default_team_set() -> Optional[Union[List[str], List[NewUserRequestTeam]]]:
|
||||
if litellm.default_internal_user_params is None:
|
||||
return None
|
||||
teams = litellm.default_internal_user_params.get("teams")
|
||||
if teams is not None:
|
||||
if all(isinstance(team, str) for team in teams):
|
||||
return teams
|
||||
elif all(isinstance(team, dict) for team in teams):
|
||||
return [
|
||||
NewUserRequestTeam(
|
||||
team_id=team.get("team_id"),
|
||||
max_budget_in_team=team.get("max_budget_in_team"),
|
||||
user_role=team.get("user_role", "user"),
|
||||
)
|
||||
for team in teams
|
||||
]
|
||||
else:
|
||||
verbose_proxy_logger.error(
|
||||
"Invalid team type in default internal user params: %s",
|
||||
teams,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@router.post(
|
||||
"/user/new",
|
||||
tags=["Internal User management"],
|
||||
@@ -334,9 +252,6 @@ async def new_user(
|
||||
|
||||
data_json = data.json() # type: ignore
|
||||
data_json = _update_internal_new_user_params(data_json, data)
|
||||
teams = data.teams
|
||||
if teams is None:
|
||||
teams = check_if_default_team_set()
|
||||
organization_ids = cast(
|
||||
Optional[List[str]], data_json.pop("organizations", None)
|
||||
)
|
||||
@@ -345,41 +260,47 @@ async def new_user(
|
||||
# Admin UI Logic
|
||||
# Add User to Team and Organization
|
||||
# if team_id passed add this user to the team
|
||||
_team_id = data_json.get("team_id", None)
|
||||
if _team_id is not None:
|
||||
await _add_user_to_team(
|
||||
user_id=cast(str, response.get("user_id")),
|
||||
team_id=_team_id,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
user_email=data.user_email,
|
||||
max_budget_in_team=None,
|
||||
user_role="user",
|
||||
if data_json.get("team_id", None) is not None:
|
||||
from litellm.proxy.management_endpoints.team_endpoints import (
|
||||
team_member_add,
|
||||
)
|
||||
elif teams is not None:
|
||||
tasks = []
|
||||
for team in teams:
|
||||
max_budget_in_team: Optional[float] = None
|
||||
user_role: Literal["user", "admin"] = "user"
|
||||
if isinstance(team, str):
|
||||
team_id = team
|
||||
elif isinstance(team, NewUserRequestTeam):
|
||||
team_id = team.team_id
|
||||
max_budget_in_team = team.max_budget_in_team
|
||||
user_role = team.user_role
|
||||
else:
|
||||
raise ValueError(f"Invalid team type: {type(team)}")
|
||||
|
||||
tasks.append(
|
||||
_add_user_to_team(
|
||||
user_id=cast(str, response.get("user_id")),
|
||||
team_id=team_id,
|
||||
user_email=data.user_email,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
max_budget_in_team=max_budget_in_team,
|
||||
user_role=user_role,
|
||||
)
|
||||
try:
|
||||
await team_member_add(
|
||||
data=TeamMemberAddRequest(
|
||||
team_id=data_json.get("team_id", None),
|
||||
member=Member(
|
||||
user_id=data_json.get("user_id", None),
|
||||
role="user",
|
||||
user_email=data_json.get("user_email", None),
|
||||
),
|
||||
),
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
except HTTPException as e:
|
||||
if e.status_code == 400 and (
|
||||
"already exists" in str(e) or "doesn't exist" in str(e)
|
||||
):
|
||||
verbose_proxy_logger.debug(
|
||||
"litellm.proxy.management_endpoints.internal_user_endpoints.new_user(): User already exists in team - {}".format(
|
||||
str(e)
|
||||
)
|
||||
)
|
||||
else:
|
||||
verbose_proxy_logger.debug(
|
||||
"litellm.proxy.management_endpoints.internal_user_endpoints.new_user(): Exception occured - {}".format(
|
||||
str(e)
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
if "already exists" in str(e) or "doesn't exist" in str(e):
|
||||
verbose_proxy_logger.debug(
|
||||
"litellm.proxy.management_endpoints.internal_user_endpoints.new_user(): User already exists in team - {}".format(
|
||||
str(e)
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise e
|
||||
|
||||
user_id = cast(Optional[str], response.get("user_id", None))
|
||||
|
||||
@@ -755,9 +676,9 @@ def _update_internal_user_params(data_json: dict, data: UpdateUserRequest) -> di
|
||||
"budget_duration" not in non_default_values
|
||||
): # applies internal user limits, if user role updated
|
||||
if is_internal_user and litellm.internal_user_budget_duration is not None:
|
||||
non_default_values["budget_duration"] = (
|
||||
litellm.internal_user_budget_duration
|
||||
)
|
||||
non_default_values[
|
||||
"budget_duration"
|
||||
] = litellm.internal_user_budget_duration
|
||||
from litellm.proxy.common_utils.timezone_utils import get_budget_reset_time
|
||||
|
||||
non_default_values["budget_reset_at"] = get_budget_reset_time(
|
||||
@@ -1401,13 +1322,13 @@ async def ui_view_users(
|
||||
}
|
||||
|
||||
# Query users with pagination and filters
|
||||
users: Optional[List[BaseModel]] = (
|
||||
await prisma_client.db.litellm_usertable.find_many(
|
||||
where=where_conditions,
|
||||
skip=skip,
|
||||
take=page_size,
|
||||
order={"created_at": "desc"},
|
||||
)
|
||||
users: Optional[
|
||||
List[BaseModel]
|
||||
] = await prisma_client.db.litellm_usertable.find_many(
|
||||
where=where_conditions,
|
||||
skip=skip,
|
||||
take=page_size,
|
||||
order={"created_at": "desc"},
|
||||
)
|
||||
|
||||
if not users:
|
||||
|
||||
@@ -349,128 +349,3 @@ async def test_user_info_url_encoding_plus_character(mocker):
|
||||
f"mock_prisma_client.get_data.call_args: {mock_prisma_client.get_data.call_args.kwargs}"
|
||||
)
|
||||
assert mock_prisma_client.get_data.call_args.kwargs["user_id"] == expected_user_id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_new_user_default_teams_flow(mocker):
|
||||
"""
|
||||
Test that when teams are set via default_internal_user_params:
|
||||
- Teams are NOT sent to generate_key_helper_fn
|
||||
- Teams ARE sent to _add_user_to_team
|
||||
"""
|
||||
import litellm
|
||||
from litellm.proxy._types import NewUserRequest, NewUserRequestTeam, UserAPIKeyAuth
|
||||
from litellm.proxy.management_endpoints.internal_user_endpoints import new_user
|
||||
|
||||
# Mock the prisma client
|
||||
mock_prisma_client = mocker.MagicMock()
|
||||
|
||||
# Setup the mock count response (under license limit)
|
||||
async def mock_count(*args, **kwargs):
|
||||
return 5 # Low user count, under limit
|
||||
|
||||
mock_prisma_client.db.litellm_usertable.count = mock_count
|
||||
|
||||
# Mock check_duplicate_user_email to pass
|
||||
async def mock_check_duplicate_user_email(*args, **kwargs):
|
||||
return None # No duplicate found
|
||||
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.internal_user_endpoints._check_duplicate_user_email",
|
||||
mock_check_duplicate_user_email,
|
||||
)
|
||||
|
||||
# Mock the license check to return False (under limit)
|
||||
mock_license_check = mocker.MagicMock()
|
||||
mock_license_check.is_over_limit.return_value = False
|
||||
|
||||
# Mock generate_key_helper_fn
|
||||
mock_generate_key_helper_fn = mocker.AsyncMock()
|
||||
mock_generate_key_helper_fn.return_value = {
|
||||
"user_id": "test-user-123",
|
||||
"token": "sk-test-token-123",
|
||||
"expires": None,
|
||||
"max_budget": 100,
|
||||
}
|
||||
|
||||
# Mock _add_user_to_team
|
||||
mock_add_user_to_team = mocker.AsyncMock()
|
||||
|
||||
# Mock UserManagementEventHooks.async_user_created_hook
|
||||
mock_user_created_hook = mocker.AsyncMock()
|
||||
|
||||
# Setup default_internal_user_params with teams
|
||||
original_default_params = getattr(litellm, "default_internal_user_params", None)
|
||||
litellm.default_internal_user_params = {
|
||||
"teams": [
|
||||
{
|
||||
"team_id": "96fed65b-0182-4ff4-8429-2721cd7d42af",
|
||||
"max_budget_in_team": 100,
|
||||
"user_role": "user",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
try:
|
||||
# Patch all the imports
|
||||
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
|
||||
mocker.patch("litellm.proxy.proxy_server._license_check", mock_license_check)
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.internal_user_endpoints.generate_key_helper_fn",
|
||||
mock_generate_key_helper_fn,
|
||||
)
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.internal_user_endpoints._add_user_to_team",
|
||||
mock_add_user_to_team,
|
||||
)
|
||||
mocker.patch(
|
||||
"litellm.proxy.management_endpoints.internal_user_endpoints.UserManagementEventHooks.async_user_created_hook",
|
||||
mock_user_created_hook,
|
||||
)
|
||||
|
||||
# Create test request data WITHOUT teams (teams should come from defaults)
|
||||
user_request = NewUserRequest(
|
||||
user_email="test@example.com", user_role="internal_user"
|
||||
)
|
||||
|
||||
# Mock user_api_key_dict
|
||||
mock_user_api_key_dict = UserAPIKeyAuth(user_id="test_admin")
|
||||
|
||||
# Call new_user function
|
||||
response = await new_user(
|
||||
data=user_request, user_api_key_dict=mock_user_api_key_dict
|
||||
)
|
||||
|
||||
# Verify generate_key_helper_fn was called WITHOUT teams
|
||||
mock_generate_key_helper_fn.assert_called_once()
|
||||
call_kwargs = mock_generate_key_helper_fn.call_args.kwargs
|
||||
|
||||
# Teams should be removed from the data passed to generate_key_helper_fn
|
||||
assert (
|
||||
"teams" not in call_kwargs
|
||||
), "Teams should not be passed to generate_key_helper_fn"
|
||||
assert call_kwargs["request_type"] == "user"
|
||||
assert call_kwargs["user_email"] == "test@example.com"
|
||||
assert call_kwargs["user_role"] == "internal_user"
|
||||
|
||||
# Verify _add_user_to_team was called with the default team
|
||||
mock_add_user_to_team.assert_called_once()
|
||||
team_call_kwargs = mock_add_user_to_team.call_args.kwargs
|
||||
|
||||
assert team_call_kwargs["user_id"] == "test-user-123"
|
||||
assert team_call_kwargs["team_id"] == "96fed65b-0182-4ff4-8429-2721cd7d42af"
|
||||
assert team_call_kwargs["user_email"] == "test@example.com"
|
||||
assert team_call_kwargs["max_budget_in_team"] == 100
|
||||
assert team_call_kwargs["user_role"] == "user"
|
||||
|
||||
# Verify response structure
|
||||
assert response.user_id == "test-user-123"
|
||||
assert response.key == "sk-test-token-123"
|
||||
|
||||
finally:
|
||||
# Restore original default params
|
||||
if original_default_params is not None:
|
||||
litellm.default_internal_user_params = original_default_params
|
||||
else:
|
||||
if hasattr(litellm, "default_internal_user_params"):
|
||||
delattr(litellm, "default_internal_user_params")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Card, Title, Text, Divider, Button, TextInput } from "@tremor/react";
|
||||
import { Typography, Spin, message, Switch, Select, Form, InputNumber } from "antd";
|
||||
import { PlusOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||
import { Typography, Spin, message, Switch, Select, Form } from "antd";
|
||||
import { getInternalUserSettings, updateInternalUserSettings, modelAvailableCall } from "./networking";
|
||||
import BudgetDurationDropdown, { getBudgetDurationLabel } from "./common_components/budget_duration_dropdown";
|
||||
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
|
||||
@@ -13,12 +12,6 @@ interface SSOSettingsProps {
|
||||
userRole: string;
|
||||
}
|
||||
|
||||
interface TeamEntry {
|
||||
team_id: string;
|
||||
max_budget_in_team?: number;
|
||||
user_role: "user" | "admin";
|
||||
}
|
||||
|
||||
const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles, userID, userRole }) => {
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [settings, setSettings] = useState<any>(null);
|
||||
@@ -87,133 +80,10 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
||||
}));
|
||||
};
|
||||
|
||||
// Helper function to normalize teams array to consistent format
|
||||
const normalizeTeams = (teams: any[]): TeamEntry[] => {
|
||||
if (!teams || !Array.isArray(teams)) return [];
|
||||
|
||||
return teams.map(team => {
|
||||
if (typeof team === "string") {
|
||||
return {
|
||||
team_id: team,
|
||||
user_role: "user" as const
|
||||
};
|
||||
} else if (typeof team === "object" && team.team_id) {
|
||||
return {
|
||||
team_id: team.team_id,
|
||||
max_budget_in_team: team.max_budget_in_team,
|
||||
user_role: team.user_role || "user"
|
||||
};
|
||||
}
|
||||
return {
|
||||
team_id: "",
|
||||
user_role: "user" as const
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Teams editor component
|
||||
const renderTeamsEditor = (teams: any[]) => {
|
||||
const normalizedTeams = normalizeTeams(teams);
|
||||
|
||||
const updateTeam = (index: number, field: keyof TeamEntry, value: any) => {
|
||||
const updatedTeams = [...normalizedTeams];
|
||||
updatedTeams[index] = {
|
||||
...updatedTeams[index],
|
||||
[field]: value
|
||||
};
|
||||
handleTextInputChange("teams", updatedTeams);
|
||||
};
|
||||
|
||||
const addTeam = () => {
|
||||
const newTeam: TeamEntry = {
|
||||
team_id: "",
|
||||
user_role: "user"
|
||||
};
|
||||
handleTextInputChange("teams", [...normalizedTeams, newTeam]);
|
||||
};
|
||||
|
||||
const removeTeam = (index: number) => {
|
||||
const updatedTeams = normalizedTeams.filter((_, i) => i !== index);
|
||||
handleTextInputChange("teams", updatedTeams);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{normalizedTeams.map((team, index) => (
|
||||
<div key={index} className="border rounded-lg p-4 bg-gray-50">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<Text className="font-medium">Team {index + 1}</Text>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={DeleteOutlined}
|
||||
onClick={() => removeTeam(index)}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-1">Team ID</Text>
|
||||
<TextInput
|
||||
value={team.team_id}
|
||||
onChange={(e) => updateTeam(index, "team_id", e.target.value)}
|
||||
placeholder="Enter team ID"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-1">Max Budget in Team</Text>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
value={team.max_budget_in_team}
|
||||
onChange={(value) => updateTeam(index, "max_budget_in_team", value)}
|
||||
placeholder="Optional"
|
||||
min={0}
|
||||
step={0.01}
|
||||
precision={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="text-sm font-medium mb-1">User Role</Text>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
value={team.user_role}
|
||||
onChange={(value) => updateTeam(index, "user_role", value)}
|
||||
>
|
||||
<Option value="user">User</Option>
|
||||
<Option value="admin">Admin</Option>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={PlusOutlined}
|
||||
onClick={addTeam}
|
||||
className="w-full"
|
||||
>
|
||||
Add Team
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderEditableField = (key: string, property: any, value: any) => {
|
||||
const type = property.type;
|
||||
|
||||
if (key === "teams") {
|
||||
return (
|
||||
<div className="mt-2">
|
||||
{renderTeamsEditor(editedValues[key] || [])}
|
||||
</div>
|
||||
);
|
||||
} else if (key === "user_role" && possibleUIRoles) {
|
||||
if (key === "user_role" && possibleUIRoles) {
|
||||
return (
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
@@ -308,39 +178,6 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken, possibleUIRoles,
|
||||
const renderValue = (key: string, value: any): JSX.Element => {
|
||||
if (value === null || value === undefined) return <span className="text-gray-400">Not set</span>;
|
||||
|
||||
if (key === "teams" && Array.isArray(value)) {
|
||||
if (value.length === 0) return <span className="text-gray-400">No teams assigned</span>;
|
||||
|
||||
const normalizedTeams = normalizeTeams(value);
|
||||
|
||||
return (
|
||||
<div className="space-y-2 mt-1">
|
||||
{normalizedTeams.map((team, index) => (
|
||||
<div key={index} className="border rounded-lg p-3 bg-white">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 text-sm">
|
||||
<div>
|
||||
<span className="font-medium text-gray-600">Team ID:</span>
|
||||
<p className="text-gray-900">{team.team_id || "Not specified"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-600">Max Budget:</span>
|
||||
<p className="text-gray-900">
|
||||
{team.max_budget_in_team !== undefined
|
||||
? `$${team.max_budget_in_team}`
|
||||
: "No limit"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-gray-600">Role:</span>
|
||||
<p className="text-gray-900 capitalize">{team.user_role}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (key === "user_role" && possibleUIRoles && possibleUIRoles[value]) {
|
||||
const { ui_label, description } = possibleUIRoles[value];
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user