forked from phoenix/litellm-mirror
Merge pull request #2183 from BerriAI/litellm_team_rate_limits
fix(proxy_server.py): allow user to set team tpm/rpm limits/budget/models
This commit is contained in:
commit
61d69b1efa
14 changed files with 958 additions and 57 deletions
|
@ -119,6 +119,51 @@ curl --location 'http://0.0.0.0:8000/key/generate' \
|
|||
--data '{"models": ["azure-models"], "user_id": "krrish3@berri.ai"}'
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="per-team" label="For Team">
|
||||
You can:
|
||||
- Add budgets to Teams
|
||||
|
||||
|
||||
#### **Add budgets to users**
|
||||
```shell
|
||||
curl --location 'http://localhost:8000/team/new' \
|
||||
--header 'Authorization: Bearer <your-master-key>' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"team_alias": "my-new-team_4",
|
||||
"members_with_roles": [{"role": "admin", "user_id": "5c4a0aa3-a1e1-43dc-bd87-3c2da8382a3a"}],
|
||||
"rpm_limit": 99
|
||||
}'
|
||||
```
|
||||
|
||||
[**See Swagger**](https://litellm-api.up.railway.app/#/team%20management/new_team_team_new_post)
|
||||
|
||||
**Sample Response**
|
||||
|
||||
```shell
|
||||
{
|
||||
"team_alias": "my-new-team_4",
|
||||
"team_id": "13e83b19-f851-43fe-8e93-f96e21033100",
|
||||
"admins": [],
|
||||
"members": [],
|
||||
"members_with_roles": [
|
||||
{
|
||||
"role": "admin",
|
||||
"user_id": "5c4a0aa3-a1e1-43dc-bd87-3c2da8382a3a"
|
||||
}
|
||||
],
|
||||
"metadata": {},
|
||||
"tpm_limit": null,
|
||||
"rpm_limit": 99,
|
||||
"max_budget": null,
|
||||
"models": [],
|
||||
"spend": 0.0,
|
||||
"max_parallel_requests": null,
|
||||
"budget_duration": null,
|
||||
"budget_reset_at": null
|
||||
}
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="per-user-chat" label="For 'user' passed to /chat/completions">
|
||||
|
||||
|
|
|
@ -230,7 +230,14 @@ class UpdateUserRequest(GenerateRequestBase):
|
|||
|
||||
class Member(LiteLLMBase):
|
||||
role: Literal["admin", "user"]
|
||||
user_id: str
|
||||
user_id: Optional[str] = None
|
||||
user_email: Optional[str] = None
|
||||
|
||||
@root_validator(pre=True)
|
||||
def check_user_info(cls, values):
|
||||
if values.get("user_id") is None and values.get("user_email") is None:
|
||||
raise ValueError("Either user id or user email must be provided")
|
||||
return values
|
||||
|
||||
|
||||
class NewTeamRequest(LiteLLMBase):
|
||||
|
@ -240,6 +247,15 @@ class NewTeamRequest(LiteLLMBase):
|
|||
members: list = []
|
||||
members_with_roles: List[Member] = []
|
||||
metadata: Optional[dict] = None
|
||||
tpm_limit: Optional[int] = None
|
||||
rpm_limit: Optional[int] = None
|
||||
max_budget: Optional[float] = None
|
||||
models: list = []
|
||||
|
||||
|
||||
class TeamMemberAddRequest(LiteLLMBase):
|
||||
team_id: str
|
||||
member: Optional[Member] = None
|
||||
|
||||
|
||||
class UpdateTeamRequest(LiteLLMBase):
|
||||
|
@ -256,23 +272,29 @@ class DeleteTeamRequest(LiteLLMBase):
|
|||
|
||||
|
||||
class LiteLLM_TeamTable(NewTeamRequest):
|
||||
max_budget: Optional[float] = None
|
||||
spend: Optional[float] = None
|
||||
models: list = []
|
||||
max_parallel_requests: Optional[int] = None
|
||||
tpm_limit: Optional[int] = None
|
||||
rpm_limit: Optional[int] = None
|
||||
budget_duration: Optional[str] = None
|
||||
budget_reset_at: Optional[datetime] = None
|
||||
|
||||
@root_validator(pre=True)
|
||||
def set_model_info(cls, values):
|
||||
dict_fields = [
|
||||
"metadata",
|
||||
"aliases",
|
||||
"config",
|
||||
"permissions",
|
||||
"model_max_budget",
|
||||
]
|
||||
for field in dict_fields:
|
||||
value = values.get(field)
|
||||
if value is not None and isinstance(value, str):
|
||||
try:
|
||||
values[field] = json.loads(value)
|
||||
except json.JSONDecodeError:
|
||||
raise ValueError(f"Field {field} should be a valid dictionary")
|
||||
|
||||
class NewTeamResponse(LiteLLMBase):
|
||||
team_id: str
|
||||
admins: list
|
||||
members: list
|
||||
metadata: dict
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
return values
|
||||
|
||||
|
||||
class TeamRequest(LiteLLMBase):
|
||||
|
|
|
@ -4063,6 +4063,7 @@ async def user_info(
|
|||
default=False,
|
||||
description="set to true to View all users. When using view_all, don't pass user_id",
|
||||
),
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Use this to get user information. (user row + all user key info)
|
||||
|
@ -4108,6 +4109,22 @@ async def user_info(
|
|||
team_id_list=user_info.teams, table_name="team", query_type="find_all"
|
||||
)
|
||||
|
||||
if teams_2 is not None and isinstance(teams_2, list):
|
||||
for team in teams_2:
|
||||
if team.team_id not in team_id_list:
|
||||
team_list.append(team)
|
||||
team_id_list.append(team.team_id)
|
||||
elif user_api_key_dict.user_id is not None:
|
||||
caller_user_info = await prisma_client.get_data(
|
||||
user_id=user_api_key_dict.user_id
|
||||
)
|
||||
# *NEW* get all teams in user 'teams' field
|
||||
teams_2 = await prisma_client.get_data(
|
||||
team_id_list=caller_user_info.teams,
|
||||
table_name="team",
|
||||
query_type="find_all",
|
||||
)
|
||||
|
||||
if teams_2 is not None and isinstance(teams_2, list):
|
||||
for team in teams_2:
|
||||
if team.team_id not in team_id_list:
|
||||
|
@ -4137,12 +4154,14 @@ async def user_info(
|
|||
# if using pydantic v1
|
||||
key = key.dict()
|
||||
key.pop("token", None)
|
||||
return {
|
||||
|
||||
response_data = {
|
||||
"user_id": user_id,
|
||||
"user_info": user_info,
|
||||
"keys": keys,
|
||||
"teams": team_list,
|
||||
}
|
||||
return response_data
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
if isinstance(e, HTTPException):
|
||||
|
@ -4401,7 +4420,7 @@ async def unblock_user(data: BlockUsers):
|
|||
"/team/new",
|
||||
tags=["team management"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=NewTeamResponse,
|
||||
response_model=LiteLLM_TeamTable,
|
||||
)
|
||||
async def new_team(
|
||||
data: NewTeamRequest,
|
||||
|
@ -4447,13 +4466,66 @@ async def new_team(
|
|||
if data.team_id is None:
|
||||
data.team_id = str(uuid.uuid4())
|
||||
|
||||
if (
|
||||
data.tpm_limit is not None
|
||||
and user_api_key_dict.tpm_limit is not None
|
||||
and data.tpm_limit > user_api_key_dict.tpm_limit
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": f"tpm limit higher than user max. User tpm limit={user_api_key_dict.tpm_limit}"
|
||||
},
|
||||
)
|
||||
|
||||
if (
|
||||
data.rpm_limit is not None
|
||||
and user_api_key_dict.rpm_limit is not None
|
||||
and data.rpm_limit > user_api_key_dict.rpm_limit
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": f"rpm limit higher than user max. User rpm limit={user_api_key_dict.rpm_limit}"
|
||||
},
|
||||
)
|
||||
|
||||
if (
|
||||
data.max_budget is not None
|
||||
and user_api_key_dict.max_budget is not None
|
||||
and data.max_budget > user_api_key_dict.max_budget
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": f"max budget higher than user max. User max budget={user_api_key_dict.max_budget}"
|
||||
},
|
||||
)
|
||||
|
||||
if data.models is not None:
|
||||
for m in data.models:
|
||||
if m not in user_api_key_dict.models:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": f"Model not in allowed user models. User allowed models={user_api_key_dict.models}"
|
||||
},
|
||||
)
|
||||
|
||||
if user_api_key_dict.user_id is not None:
|
||||
creating_user_in_list = False
|
||||
for member in data.members_with_roles:
|
||||
if member.user_id == user_api_key_dict.user_id:
|
||||
creating_user_in_list = True
|
||||
|
||||
if creating_user_in_list == False:
|
||||
data.members_with_roles.append(
|
||||
Member(role="admin", user_id=user_api_key_dict.user_id)
|
||||
)
|
||||
|
||||
complete_team_data = LiteLLM_TeamTable(
|
||||
**data.json(),
|
||||
max_budget=user_api_key_dict.max_budget,
|
||||
models=user_api_key_dict.models,
|
||||
max_parallel_requests=user_api_key_dict.max_parallel_requests,
|
||||
tpm_limit=user_api_key_dict.tpm_limit,
|
||||
rpm_limit=user_api_key_dict.rpm_limit,
|
||||
budget_duration=user_api_key_dict.budget_duration,
|
||||
budget_reset_at=user_api_key_dict.budget_reset_at,
|
||||
)
|
||||
|
@ -4468,13 +4540,13 @@ async def new_team(
|
|||
await prisma_client.update_data(
|
||||
user_id=user.user_id,
|
||||
data={"user_id": user.user_id, "teams": [team_row.team_id]},
|
||||
update_key_values={
|
||||
update_key_values_custom_query={
|
||||
"teams": {
|
||||
"push ": [team_row.team_id],
|
||||
}
|
||||
},
|
||||
)
|
||||
return team_row
|
||||
return team_row.model_dump()
|
||||
|
||||
|
||||
@router.post(
|
||||
|
@ -4485,6 +4557,9 @@ async def update_team(
|
|||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
[BETA]
|
||||
[DEPRECATED] - use the `/team/member_add` and `/team/member_remove` endpoints instead
|
||||
|
||||
You can now add / delete users from a team via /team/update
|
||||
|
||||
```
|
||||
|
@ -4524,7 +4599,8 @@ async def update_team(
|
|||
existing_user_id_list = []
|
||||
## Get new users
|
||||
for user in existing_team_row.members_with_roles:
|
||||
existing_user_id_list.append(user["user_id"])
|
||||
if user["user_id"] is not None:
|
||||
existing_user_id_list.append(user["user_id"])
|
||||
|
||||
## Update new user rows with team id (info used by /user/info to show all teams, user is a part of)
|
||||
if data.members_with_roles is not None:
|
||||
|
@ -4532,26 +4608,32 @@ async def update_team(
|
|||
if user.user_id not in existing_user_id_list:
|
||||
await prisma_client.update_data(
|
||||
user_id=user.user_id,
|
||||
data={"user_id": user.user_id, "teams": [team_row["team_id"]]},
|
||||
update_key_values={
|
||||
data={
|
||||
"user_id": user.user_id,
|
||||
"teams": [team_row["team_id"]],
|
||||
"models": team_row["data"].models,
|
||||
},
|
||||
update_key_values_custom_query={
|
||||
"teams": {
|
||||
"push": [team_row["team_id"]],
|
||||
}
|
||||
},
|
||||
table_name="user",
|
||||
)
|
||||
|
||||
## REMOVE DELETED USERS ##
|
||||
### Get list of deleted users (old list - new list)
|
||||
deleted_user_id_list = []
|
||||
existing_user_id_list = []
|
||||
new_user_id_list = []
|
||||
## Get old user list
|
||||
for user in existing_team_row.members_with_roles:
|
||||
existing_user_id_list.append(user["user_id"])
|
||||
## Get diff
|
||||
if data.members_with_roles is not None:
|
||||
for user in data.members_with_roles:
|
||||
if user.user_id not in existing_user_id_list:
|
||||
deleted_user_id_list.append(user.user_id)
|
||||
new_user_id_list.append(user.user_id)
|
||||
## Get diff
|
||||
if existing_team_row.members_with_roles is not None:
|
||||
for user in existing_team_row.members_with_roles:
|
||||
if user["user_id"] not in new_user_id_list:
|
||||
deleted_user_id_list.append(user["user_id"])
|
||||
|
||||
## SET UPDATED LIST
|
||||
if len(deleted_user_id_list) > 0:
|
||||
|
@ -4570,6 +4652,99 @@ async def update_team(
|
|||
return team_row
|
||||
|
||||
|
||||
@router.post(
|
||||
"/team/member_add",
|
||||
tags=["team management"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def team_member_add(
|
||||
data: TeamMemberAddRequest,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
[BETA]
|
||||
|
||||
Add new members (either via user_email or user_id) to a team
|
||||
|
||||
If user doesn't exist, new user row will also be added to User Table
|
||||
|
||||
```
|
||||
curl -X POST 'http://0.0.0.0:8000/team/update' \
|
||||
|
||||
-H 'Authorization: Bearer sk-1234' \
|
||||
|
||||
-H 'Content-Type: application/json' \
|
||||
|
||||
-D '{
|
||||
"team_id": "45e3e396-ee08-4a61-a88e-16b3ce7e0849",
|
||||
"member": {"role": "user", "user_id": "krrish247652@berri.ai"}
|
||||
}'
|
||||
```
|
||||
"""
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail={"error": "No db connected"})
|
||||
|
||||
if data.team_id is None:
|
||||
raise HTTPException(status_code=400, detail={"error": "No team id passed in"})
|
||||
|
||||
if data.member is None:
|
||||
raise HTTPException(status_code=400, detail={"error": "No member passed in"})
|
||||
|
||||
existing_team_row = await prisma_client.get_data( # type: ignore
|
||||
team_id=data.team_id, table_name="team", query_type="find_unique"
|
||||
)
|
||||
|
||||
new_member = data.member
|
||||
|
||||
existing_team_row.members_with_roles.append(new_member)
|
||||
|
||||
complete_team_data = LiteLLM_TeamTable(
|
||||
**existing_team_row.model_dump(),
|
||||
)
|
||||
|
||||
team_row = await prisma_client.update_data(
|
||||
update_key_values=complete_team_data.json(exclude_none=True),
|
||||
data=complete_team_data.json(exclude_none=True),
|
||||
table_name="team",
|
||||
team_id=data.team_id,
|
||||
)
|
||||
|
||||
## ADD USER, IF NEW ##
|
||||
user_data = { # type: ignore
|
||||
"teams": [team_row["team_id"]],
|
||||
"models": team_row["data"].models,
|
||||
}
|
||||
if new_member.user_id is not None:
|
||||
user_data["user_id"] = new_member.user_id # type: ignore
|
||||
await prisma_client.update_data(
|
||||
user_id=new_member.user_id,
|
||||
data=user_data,
|
||||
update_key_values_custom_query={
|
||||
"teams": {
|
||||
"push": [team_row["team_id"]],
|
||||
}
|
||||
},
|
||||
table_name="user",
|
||||
)
|
||||
elif new_member.user_email is not None:
|
||||
user_data["user_id"] = str(uuid.uuid4())
|
||||
user_data["user_email"] = new_member.user_email
|
||||
## user email is not unique acc. to prisma schema -> future improvement
|
||||
### for now: check if it exists in db, if not - insert it
|
||||
existing_user_row = await prisma_client.get_data(
|
||||
key_val={"user_email": new_member.user_email},
|
||||
table_name="user",
|
||||
query_type="find_all",
|
||||
)
|
||||
if existing_user_row is None or (
|
||||
isinstance(existing_user_row, list) and len(existing_user_row) == 0
|
||||
):
|
||||
|
||||
await prisma_client.insert_data(data=user_data, table_name="user")
|
||||
|
||||
return team_row
|
||||
|
||||
|
||||
@router.post(
|
||||
"/team/delete", tags=["team management"], dependencies=[Depends(user_api_key_auth)]
|
||||
)
|
||||
|
|
|
@ -635,11 +635,15 @@ class PrismaClient:
|
|||
table_name is not None and table_name == "user"
|
||||
):
|
||||
if query_type == "find_unique":
|
||||
if key_val is None:
|
||||
key_val = {"user_id": user_id}
|
||||
response = await self.db.litellm_usertable.find_unique( # type: ignore
|
||||
where={
|
||||
"user_id": user_id, # type: ignore
|
||||
}
|
||||
where=key_val # type: ignore
|
||||
)
|
||||
elif query_type == "find_all" and key_val is not None:
|
||||
response = await self.db.litellm_usertable.find_many(
|
||||
where=key_val # type: ignore
|
||||
) # type: ignore
|
||||
elif query_type == "find_all" and reset_at is not None:
|
||||
response = await self.db.litellm_usertable.find_many(
|
||||
where={ # type:ignore
|
||||
|
@ -869,12 +873,15 @@ class PrismaClient:
|
|||
query_type: Literal["update", "update_many"] = "update",
|
||||
table_name: Optional[Literal["user", "key", "config", "spend", "team"]] = None,
|
||||
update_key_values: Optional[dict] = None,
|
||||
update_key_values_custom_query: Optional[dict] = None,
|
||||
):
|
||||
"""
|
||||
Update existing data
|
||||
"""
|
||||
try:
|
||||
db_data = self.jsonify_object(data=data)
|
||||
if update_key_values is not None:
|
||||
update_key_values = self.jsonify_object(data=update_key_values)
|
||||
if token is not None:
|
||||
print_verbose(f"token: {token}")
|
||||
# check if plain text or hash
|
||||
|
@ -902,7 +909,10 @@ class PrismaClient:
|
|||
if user_id is None:
|
||||
user_id = db_data["user_id"]
|
||||
if update_key_values is None:
|
||||
update_key_values = db_data
|
||||
if update_key_values_custom_query is not None:
|
||||
update_key_values = update_key_values_custom_query
|
||||
else:
|
||||
update_key_values = db_data
|
||||
update_user_row = await self.db.litellm_usertable.upsert(
|
||||
where={"user_id": user_id}, # type: ignore
|
||||
data={
|
||||
|
@ -944,7 +954,6 @@ class PrismaClient:
|
|||
update_key_values["members_with_roles"] = json.dumps(
|
||||
update_key_values["members_with_roles"]
|
||||
)
|
||||
|
||||
update_team_row = await self.db.litellm_teamtable.upsert(
|
||||
where={"team_id": team_id}, # type: ignore
|
||||
data={
|
||||
|
|
|
@ -5,14 +5,16 @@ import Navbar from "../components/navbar";
|
|||
import UserDashboard from "../components/user_dashboard";
|
||||
import ModelDashboard from "@/components/model_dashboard";
|
||||
import ViewUserDashboard from "@/components/view_users";
|
||||
import Teams from "@/components/teams";
|
||||
import ChatUI from "@/components/chat_ui";
|
||||
import Sidebar from "../components/leftnav";
|
||||
import Usage from "../components/usage";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
|
||||
const CreateKeyPage = () => {
|
||||
const [userRole, setUserRole] = useState('');
|
||||
const [userRole, setUserRole] = useState("");
|
||||
const [userEmail, setUserEmail] = useState<null | string>(null);
|
||||
const [teams, setTeams] = useState<null | any[]>(null);
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const userID = searchParams.get("userID");
|
||||
|
@ -74,14 +76,20 @@ const CreateKeyPage = () => {
|
|||
<div className="flex flex-col min-h-screen">
|
||||
<Navbar userID={userID} userRole={userRole} userEmail={userEmail} />
|
||||
<div className="flex flex-1 overflow-auto">
|
||||
<Sidebar setPage={setPage} userRole={userRole}/>
|
||||
<Sidebar
|
||||
setPage={setPage}
|
||||
userRole={userRole}
|
||||
defaultSelectedKey={null}
|
||||
/>
|
||||
{page == "api-keys" ? (
|
||||
<UserDashboard
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
teams={teams}
|
||||
setUserRole={setUserRole}
|
||||
userEmail={userEmail}
|
||||
setUserEmail={setUserEmail}
|
||||
setTeams={setTeams}
|
||||
/>
|
||||
) : page == "models" ? (
|
||||
<ModelDashboard
|
||||
|
@ -97,16 +105,21 @@ const CreateKeyPage = () => {
|
|||
token={token}
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
)
|
||||
: page == "users" ? (
|
||||
) : page == "users" ? (
|
||||
<ViewUserDashboard
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
token={token}
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
) : page == "teams" ? (
|
||||
<Teams
|
||||
teams={teams}
|
||||
setTeams={setTeams}
|
||||
searchParams={searchParams}
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
) : (
|
||||
<Usage
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
|
|
149
ui/litellm-dashboard/src/app/team/page.tsx
Normal file
149
ui/litellm-dashboard/src/app/team/page.tsx
Normal file
|
@ -0,0 +1,149 @@
|
|||
"use client";
|
||||
import React, { Suspense, useEffect, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import Navbar from "../../components/navbar";
|
||||
import Sidebar from "../../components/leftnav";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
Card,
|
||||
Icon,
|
||||
Button,
|
||||
Col,
|
||||
Grid,
|
||||
} from "@tremor/react";
|
||||
import { CogIcon } from "@heroicons/react/outline";
|
||||
|
||||
const TeamSettingsPage = () => {
|
||||
const [userRole, setUserRole] = useState("");
|
||||
const [userEmail, setUserEmail] = useState<null | string>(null);
|
||||
const [teams, setTeams] = useState<null | string[]>(null);
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const userID = searchParams.get("userID");
|
||||
const token = searchParams.get("token");
|
||||
|
||||
const [page, setPage] = useState("team");
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
const decoded = jwtDecode(token) as { [key: string]: any };
|
||||
if (decoded) {
|
||||
// cast decoded to dictionary
|
||||
console.log("Decoded token:", decoded);
|
||||
|
||||
console.log("Decoded key:", decoded.key);
|
||||
// set accessToken
|
||||
setAccessToken(decoded.key);
|
||||
|
||||
// check if userRole is defined
|
||||
if (decoded.user_role) {
|
||||
const formattedUserRole = formatUserRole(decoded.user_role);
|
||||
console.log("Decoded user_role:", formattedUserRole);
|
||||
setUserRole(formattedUserRole);
|
||||
} else {
|
||||
console.log("User role not defined");
|
||||
}
|
||||
|
||||
if (decoded.user_email) {
|
||||
setUserEmail(decoded.user_email);
|
||||
} else {
|
||||
console.log(`User Email is not set ${decoded}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
function formatUserRole(userRole: string) {
|
||||
if (!userRole) {
|
||||
return "Undefined Role";
|
||||
}
|
||||
console.log(`Received user role: ${userRole}`);
|
||||
switch (userRole.toLowerCase()) {
|
||||
case "app_owner":
|
||||
return "App Owner";
|
||||
case "demo_app_owner":
|
||||
return "App Owner";
|
||||
case "app_admin":
|
||||
return "Admin";
|
||||
case "app_user":
|
||||
return "App User";
|
||||
default:
|
||||
return "Unknown Role";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Navbar userID={userID} userRole={userRole} userEmail={userEmail} />
|
||||
<div className="flex flex-1 overflow-auto">
|
||||
<Grid numItems={1} className="gap-0 p-10 h-[75vh] w-full">
|
||||
<Col numColSpan={1}>
|
||||
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh]">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Team Name</TableHeaderCell>
|
||||
<TableHeaderCell>Spend (USD)</TableHeaderCell>
|
||||
<TableHeaderCell>Budget (USD)</TableHeaderCell>
|
||||
<TableHeaderCell>TPM / RPM Limits</TableHeaderCell>
|
||||
<TableHeaderCell>Settings</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>Wilhelm Tell</TableCell>
|
||||
<TableCell className="text-right">1</TableCell>
|
||||
<TableCell>Uri, Schwyz, Unterwalden</TableCell>
|
||||
<TableCell>National Hero</TableCell>
|
||||
<TableCell>
|
||||
<Icon icon={CogIcon} size="sm" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>The Witcher</TableCell>
|
||||
<TableCell className="text-right">129</TableCell>
|
||||
<TableCell>Kaedwen</TableCell>
|
||||
<TableCell>Legend</TableCell>
|
||||
<TableCell>
|
||||
<Icon icon={CogIcon} size="sm" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Mizutsune</TableCell>
|
||||
<TableCell className="text-right">82</TableCell>
|
||||
<TableCell>Japan</TableCell>
|
||||
<TableCell>N/A</TableCell>
|
||||
<TableCell>
|
||||
<Icon icon={CogIcon} size="sm" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col numColSpan={1}>
|
||||
<Link
|
||||
href={`/team?userID=${searchParams.get(
|
||||
"userID"
|
||||
)}&token=${searchParams.get("token")}`}
|
||||
>
|
||||
<Button className="mx-auto">+ Create New Team</Button>
|
||||
</Link>
|
||||
</Col>
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
export default TeamSettingsPage;
|
|
@ -18,6 +18,7 @@ const { Option } = Select;
|
|||
|
||||
interface CreateKeyProps {
|
||||
userID: string;
|
||||
teamID: string | null;
|
||||
userRole: string | null;
|
||||
accessToken: string;
|
||||
data: any[] | null;
|
||||
|
@ -27,6 +28,7 @@ interface CreateKeyProps {
|
|||
|
||||
const CreateKey: React.FC<CreateKeyProps> = ({
|
||||
userID,
|
||||
teamID,
|
||||
userRole,
|
||||
accessToken,
|
||||
data,
|
||||
|
@ -36,7 +38,6 @@ const CreateKey: React.FC<CreateKeyProps> = ({
|
|||
const [form] = Form.useForm();
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [apiKey, setApiKey] = useState(null);
|
||||
|
||||
const handleOk = () => {
|
||||
setIsModalVisible(false);
|
||||
form.resetFields();
|
||||
|
@ -89,7 +90,10 @@ const CreateKey: React.FC<CreateKeyProps> = ({
|
|||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="Team ID" name="team_id">
|
||||
<Input placeholder="ai_team" />
|
||||
<Input
|
||||
placeholder="ai_team"
|
||||
defaultValue={teamID ? teamID : ""}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Models" name="models">
|
||||
<Select
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Typography } from "antd";
|
||||
import { Select, SelectItem } from "@tremor/react";
|
||||
|
||||
interface DashboardTeamProps {
|
||||
teams: Object[] | null;
|
||||
setSelectedTeam: React.Dispatch<React.SetStateAction<any | null>>;
|
||||
}
|
||||
|
||||
const DashboardTeam: React.FC<DashboardTeamProps> = ({
|
||||
teams,
|
||||
setSelectedTeam,
|
||||
}) => {
|
||||
const { Title, Paragraph } = Typography;
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
return (
|
||||
<div className="mt-10">
|
||||
<Title level={4}>Default Team</Title>
|
||||
<Paragraph>
|
||||
If you belong to multiple teams, this setting controls which team is
|
||||
used by default when creating new API Keys.
|
||||
</Paragraph>
|
||||
{teams && teams.length > 0 ? (
|
||||
<Select defaultValue="0">
|
||||
{teams.map((team: any, index) => (
|
||||
<SelectItem
|
||||
value={String(index)}
|
||||
onClick={() => setSelectedTeam(team)}
|
||||
>
|
||||
{team["team_alias"]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
) : (
|
||||
<Paragraph>
|
||||
No team created. <b>Defaulting to personal account.</b>
|
||||
</Paragraph>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardTeam;
|
|
@ -1,5 +1,6 @@
|
|||
import { Layout, Menu } from "antd";
|
||||
import Link from "next/link";
|
||||
import { List } from "postcss/lib/list";
|
||||
|
||||
const { Sider } = Layout;
|
||||
|
||||
|
@ -7,15 +8,20 @@ const { Sider } = Layout;
|
|||
interface SidebarProps {
|
||||
setPage: React.Dispatch<React.SetStateAction<string>>;
|
||||
userRole: string;
|
||||
defaultSelectedKey: string[] | null;
|
||||
}
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ setPage, userRole }) => {
|
||||
const Sidebar: React.FC<SidebarProps> = ({
|
||||
setPage,
|
||||
userRole,
|
||||
defaultSelectedKey,
|
||||
}) => {
|
||||
return (
|
||||
<Layout style={{ minHeight: "100vh", maxWidth: "120px" }}>
|
||||
<Sider width={120}>
|
||||
<Menu
|
||||
mode="inline"
|
||||
defaultSelectedKeys={["1"]}
|
||||
defaultSelectedKeys={defaultSelectedKey ? defaultSelectedKey : ["1"]}
|
||||
style={{ height: "100%", borderRight: 0 }}
|
||||
>
|
||||
<Menu.Item key="1" onClick={() => setPage("api-keys")}>
|
||||
|
@ -30,13 +36,16 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, userRole }) => {
|
|||
<Menu.Item key="4" onClick={() => setPage("usage")}>
|
||||
Usage
|
||||
</Menu.Item>
|
||||
{
|
||||
userRole == "Admin" ?
|
||||
<Menu.Item key="5" onClick={() => setPage("users")}>
|
||||
Users
|
||||
</Menu.Item>
|
||||
: null
|
||||
}
|
||||
{userRole == "Admin" ? (
|
||||
<Menu.Item key="5" onClick={() => setPage("users")}>
|
||||
Users
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
{userRole == "Admin" ? (
|
||||
<Menu.Item key="6" onClick={() => setPage("teams")}>
|
||||
Teams
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
</Menu>
|
||||
</Sider>
|
||||
</Layout>
|
||||
|
|
|
@ -29,14 +29,19 @@ const Navbar: React.FC<NavbarProps> = ({ userID, userRole, userEmail }) => {
|
|||
const isLocal = process.env.NODE_ENV === "development";
|
||||
const imageUrl = isLocal ? "http://localhost:4000/get_image" : "/get_image";
|
||||
|
||||
|
||||
return (
|
||||
<nav className="left-0 right-0 top-0 flex justify-between items-center h-12 mb-4">
|
||||
<div className="text-left mx-4 my-2 absolute top-0 left-0">
|
||||
<div className="text-left my-2 absolute top-0 left-0">
|
||||
<div className="flex flex-col items-center">
|
||||
<Link href="/">
|
||||
<button className="text-gray-800 text-2xl px-4 py-1 rounded text-center">
|
||||
<img src={imageUrl} width={200} height={200} alt="LiteLLM Brand" className="mr-2" />
|
||||
<button className="text-gray-800 text-2xl py-1 rounded text-center">
|
||||
<img
|
||||
src={imageUrl}
|
||||
width={200}
|
||||
height={200}
|
||||
alt="LiteLLM Brand"
|
||||
className="mr-2"
|
||||
/>
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
@ -196,6 +196,7 @@ export const userInfoCall = async (
|
|||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("API Response:", data);
|
||||
message.info("Received user data");
|
||||
return data;
|
||||
// Handle success - you might want to update some state or UI based on the created key
|
||||
|
@ -466,3 +467,85 @@ export const userGetRequesedtModelsCall = async (accessToken: String) => {
|
|||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const teamCreateCall = async (
|
||||
accessToken: string,
|
||||
formValues: Record<string, any> // Assuming formValues is an object
|
||||
) => {
|
||||
try {
|
||||
console.log("Form Values in teamCreateCall:", formValues); // Log the form values before making the API call
|
||||
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/team/new` : `/team/new`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...formValues, // Include formValues in the request body
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text();
|
||||
message.error("Failed to create key: " + errorData);
|
||||
console.error("Error response from the server:", errorData);
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("API Response:", data);
|
||||
return data;
|
||||
// Handle success - you might want to update some state or UI based on the created key
|
||||
} catch (error) {
|
||||
console.error("Failed to create key:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Member {
|
||||
role: string;
|
||||
user_id: string | null;
|
||||
user_email: string | null;
|
||||
}
|
||||
|
||||
export const teamMemberAddCall = async (
|
||||
accessToken: string,
|
||||
teamId: string,
|
||||
formValues: Member // Assuming formValues is an object
|
||||
) => {
|
||||
try {
|
||||
console.log("Form Values in teamMemberAddCall:", formValues); // Log the form values before making the API call
|
||||
|
||||
const url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/team/member_add`
|
||||
: `/team/member_add`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
team_id: teamId,
|
||||
member: formValues, // Include formValues in the request body
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text();
|
||||
message.error("Failed to create key: " + errorData);
|
||||
console.error("Error response from the server:", errorData);
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("API Response:", data);
|
||||
return data;
|
||||
// Handle success - you might want to update some state or UI based on the created key
|
||||
} catch (error) {
|
||||
console.error("Failed to create key:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
328
ui/litellm-dashboard/src/components/teams.tsx
Normal file
328
ui/litellm-dashboard/src/components/teams.tsx
Normal file
|
@ -0,0 +1,328 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { Typography } from "antd";
|
||||
import {
|
||||
Button as Button2,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select as Select2,
|
||||
InputNumber,
|
||||
message,
|
||||
} from "antd";
|
||||
import { Select, SelectItem } from "@tremor/react";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
Card,
|
||||
Icon,
|
||||
Button,
|
||||
Col,
|
||||
Text,
|
||||
Grid,
|
||||
} from "@tremor/react";
|
||||
import { CogIcon } from "@heroicons/react/outline";
|
||||
interface TeamProps {
|
||||
teams: any[] | null;
|
||||
searchParams: any;
|
||||
accessToken: string | null;
|
||||
setTeams: React.Dispatch<React.SetStateAction<Object[] | null>>;
|
||||
}
|
||||
import { teamCreateCall, teamMemberAddCall, Member } from "./networking";
|
||||
|
||||
const Team: React.FC<TeamProps> = ({
|
||||
teams,
|
||||
searchParams,
|
||||
accessToken,
|
||||
setTeams,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [memberForm] = Form.useForm();
|
||||
const { Title, Paragraph } = Typography;
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
const [selectedTeam, setSelectedTeam] = useState<null | any>(
|
||||
teams ? teams[0] : null
|
||||
);
|
||||
const [isTeamModalVisible, setIsTeamModalVisible] = useState(false);
|
||||
const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false);
|
||||
const handleOk = () => {
|
||||
setIsTeamModalVisible(false);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
const handleMemberOk = () => {
|
||||
setIsAddMemberModalVisible(false);
|
||||
memberForm.resetFields();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsTeamModalVisible(false);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
const handleMemberCancel = () => {
|
||||
setIsAddMemberModalVisible(false);
|
||||
memberForm.resetFields();
|
||||
};
|
||||
|
||||
const handleCreate = async (formValues: Record<string, any>) => {
|
||||
try {
|
||||
if (accessToken != null) {
|
||||
message.info("Making API Call");
|
||||
const response: any = await teamCreateCall(accessToken, formValues);
|
||||
if (teams !== null) {
|
||||
setTeams([...teams, response]);
|
||||
} else {
|
||||
setTeams([response]);
|
||||
}
|
||||
console.log(`response for team create call: ${response}`);
|
||||
setIsTeamModalVisible(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating the key:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMemberCreate = async (formValues: Record<string, any>) => {
|
||||
try {
|
||||
if (accessToken != null && teams != null) {
|
||||
message.info("Making API Call");
|
||||
const user_role: Member = {
|
||||
role: "user",
|
||||
user_email: formValues.user_email,
|
||||
user_id: null,
|
||||
};
|
||||
const response: any = await teamMemberAddCall(
|
||||
accessToken,
|
||||
selectedTeam["team_id"],
|
||||
user_role
|
||||
);
|
||||
console.log(`response for team create call: ${response["data"]}`);
|
||||
// Checking if the team exists in the list and updating or adding accordingly
|
||||
const foundIndex = teams.findIndex((team) => {
|
||||
console.log(
|
||||
`team.team_id=${team.team_id}; response.data.team_id=${response.data.team_id}`
|
||||
);
|
||||
return team.team_id === response.data.team_id;
|
||||
});
|
||||
console.log(`foundIndex: ${foundIndex}`);
|
||||
if (foundIndex !== -1) {
|
||||
// If the team is found, update it
|
||||
const updatedTeams = [...teams]; // Copy the current state
|
||||
updatedTeams[foundIndex] = response.data; // Update the specific team
|
||||
setTeams(updatedTeams); // Set the new state
|
||||
setSelectedTeam(response.data);
|
||||
}
|
||||
setIsAddMemberModalVisible(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating the key:", error);
|
||||
}
|
||||
};
|
||||
console.log(`received teams ${teams}`);
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Grid numItems={1} className="gap-2 p-2 h-[75vh] w-full">
|
||||
<Col numColSpan={1}>
|
||||
<Title level={4}>All Teams</Title>
|
||||
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh]">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Team Name</TableHeaderCell>
|
||||
<TableHeaderCell>Spend (USD)</TableHeaderCell>
|
||||
<TableHeaderCell>Budget (USD)</TableHeaderCell>
|
||||
<TableHeaderCell>TPM / RPM Limits</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{teams && teams.length > 0
|
||||
? teams.map((team: any) => (
|
||||
<TableRow>
|
||||
<TableCell>{team["team_alias"]}</TableCell>
|
||||
<TableCell>{team["spend"]}</TableCell>
|
||||
<TableCell>
|
||||
{team["max_budget"] ? team["max_budget"] : "No limit"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text>
|
||||
TPM Limit:{" "}
|
||||
{team.tpm_limit ? team.tpm_limit : "Unlimited"}{" "}
|
||||
<br></br> RPM Limit:{" "}
|
||||
{team.rpm_limit ? team.rpm_limit : "Unlimited"}
|
||||
</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Icon icon={CogIcon} size="sm" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
: null}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col numColSpan={1}>
|
||||
<Button
|
||||
className="mx-auto"
|
||||
onClick={() => setIsTeamModalVisible(true)}
|
||||
>
|
||||
+ Create New Team
|
||||
</Button>
|
||||
<Modal
|
||||
title="Create Team"
|
||||
visible={isTeamModalVisible}
|
||||
width={800}
|
||||
footer={null}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleCreate}
|
||||
labelCol={{ span: 8 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
labelAlign="left"
|
||||
>
|
||||
<>
|
||||
<Form.Item label="Team Name" name="team_alias">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label="Models" name="models">
|
||||
<Select2
|
||||
mode="multiple"
|
||||
placeholder="Select models"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
{/* {userModels.map((model) => (
|
||||
<Option key={model} value={model}>
|
||||
{model}
|
||||
</Option>
|
||||
))} */}
|
||||
</Select2>
|
||||
</Form.Item>
|
||||
<Form.Item label="Max Budget (USD)" name="max_budget">
|
||||
<InputNumber step={0.01} precision={2} width={200} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Tokens per minute Limit (TPM)"
|
||||
name="tpm_limit"
|
||||
>
|
||||
<InputNumber step={1} width={400} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Requests per minute Limit (RPM)"
|
||||
name="rpm_limit"
|
||||
>
|
||||
<InputNumber step={1} width={400} />
|
||||
</Form.Item>
|
||||
</>
|
||||
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||
<Button2 htmlType="submit">Create Team</Button2>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Col>
|
||||
<Col numColSpan={1}>
|
||||
<Title level={4}>Team Members</Title>
|
||||
<Paragraph>
|
||||
If you belong to multiple teams, this setting controls which teams'
|
||||
members you see.
|
||||
</Paragraph>
|
||||
{teams && teams.length > 0 ? (
|
||||
<Select defaultValue="0">
|
||||
{teams.map((team: any, index) => (
|
||||
<SelectItem
|
||||
value={String(index)}
|
||||
onClick={() => {
|
||||
setSelectedTeam(team);
|
||||
}}
|
||||
>
|
||||
{team["team_alias"]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
) : (
|
||||
<Paragraph>
|
||||
No team created. <b>Defaulting to personal account.</b>
|
||||
</Paragraph>
|
||||
)}
|
||||
</Col>
|
||||
<Col numColSpan={1}>
|
||||
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh]">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Member Name</TableHeaderCell>
|
||||
<TableHeaderCell>Role</TableHeaderCell>
|
||||
<TableHeaderCell>Action</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{selectedTeam
|
||||
? selectedTeam["members_with_roles"].map((member: any) => (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
{member["user_email"]
|
||||
? member["user_email"]
|
||||
: member["user_id"]
|
||||
? member["user_id"]
|
||||
: null}
|
||||
</TableCell>
|
||||
<TableCell>{member["role"]}</TableCell>
|
||||
<TableCell>
|
||||
<Icon icon={CogIcon} size="sm" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
: null}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col numColSpan={1}>
|
||||
<Button
|
||||
className="mx-auto mb-5"
|
||||
onClick={() => setIsAddMemberModalVisible(true)}
|
||||
>
|
||||
+ Add member
|
||||
</Button>
|
||||
<Modal
|
||||
title="Add member"
|
||||
visible={isAddMemberModalVisible}
|
||||
width={800}
|
||||
footer={null}
|
||||
onOk={handleMemberOk}
|
||||
onCancel={handleMemberCancel}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleMemberCreate}
|
||||
labelCol={{ span: 8 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
labelAlign="left"
|
||||
>
|
||||
<>
|
||||
<Form.Item label="Email" name="user_email">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</>
|
||||
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||
<Button2 htmlType="submit">Add member</Button2>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Col>
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Team;
|
|
@ -5,6 +5,7 @@ import { Grid, Col, Card, Text } from "@tremor/react";
|
|||
import CreateKey from "./create_key_button";
|
||||
import ViewKeyTable from "./view_key_table";
|
||||
import ViewUserSpend from "./view_user_spend";
|
||||
import DashboardTeam from "./dashboard_default_team";
|
||||
import EnterProxyUrl from "./enter_proxy_url";
|
||||
import { message } from "antd";
|
||||
import Navbar from "./navbar";
|
||||
|
@ -24,16 +25,20 @@ interface UserDashboardProps {
|
|||
userID: string | null;
|
||||
userRole: string | null;
|
||||
userEmail: string | null;
|
||||
teams: any[] | null;
|
||||
setUserRole: React.Dispatch<React.SetStateAction<string>>;
|
||||
setUserEmail: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
setTeams: React.Dispatch<React.SetStateAction<Object[] | null>>;
|
||||
}
|
||||
|
||||
const UserDashboard: React.FC<UserDashboardProps> = ({
|
||||
userID,
|
||||
userRole,
|
||||
teams,
|
||||
setUserRole,
|
||||
userEmail,
|
||||
setUserEmail,
|
||||
setTeams,
|
||||
}) => {
|
||||
const [data, setData] = useState<null | any[]>(null); // Keep the initialization of state here
|
||||
const [userSpendData, setUserSpendData] = useState<UserSpendData | null>(
|
||||
|
@ -48,6 +53,9 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
|
|||
const token = searchParams.get("token");
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
const [userModels, setUserModels] = useState<string[]>([]);
|
||||
const [selectedTeam, setSelectedTeam] = useState<any | null>(
|
||||
teams ? teams[0] : null
|
||||
);
|
||||
// check if window is not undefined
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("beforeunload", function () {
|
||||
|
@ -111,8 +119,14 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
|
|||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await userInfoCall(accessToken, userID, userRole);
|
||||
console.log(
|
||||
`received teams in user dashboard: ${Object.keys(
|
||||
response
|
||||
)}; team values: ${Object.entries(response.teams)}`
|
||||
);
|
||||
setUserSpendData(response["user_info"]);
|
||||
setData(response["keys"]); // Assuming this is the correct path to your data
|
||||
setTeams(response["teams"]);
|
||||
sessionStorage.setItem(
|
||||
"userData" + userID,
|
||||
JSON.stringify(response["keys"])
|
||||
|
@ -185,12 +199,14 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
|
|||
/>
|
||||
<CreateKey
|
||||
userID={userID}
|
||||
teamID={selectedTeam ? selectedTeam["team_id"] : null}
|
||||
userRole={userRole}
|
||||
userModels={userModels}
|
||||
accessToken={accessToken}
|
||||
data={data}
|
||||
setData={setData}
|
||||
/>
|
||||
<DashboardTeam teams={teams} setSelectedTeam={setSelectedTeam} />
|
||||
</Col>
|
||||
</Grid>
|
||||
</div>
|
||||
|
|
|
@ -43,7 +43,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
|
||||
// Set the key to delete and open the confirmation modal
|
||||
setKeyToDelete(token);
|
||||
localStorage.removeItem("userData" + userID)
|
||||
localStorage.removeItem("userData" + userID);
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
|
||||
|
@ -79,7 +79,6 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
console.log("RERENDER TRIGGERED");
|
||||
return (
|
||||
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh] mb-4">
|
||||
<Title>API Keys</Title>
|
||||
<Table className="mt-5">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue