diff --git a/litellm/proxy/_experimental/out/onboarding.html b/litellm/proxy/_experimental/out/onboarding.html deleted file mode 100644 index 83def09aa2..0000000000 --- a/litellm/proxy/_experimental/out/onboarding.html +++ /dev/null @@ -1 +0,0 @@ -LiteLLM Dashboard \ No newline at end of file diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 00ea8fb7f4..4c296a5798 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -1487,6 +1487,7 @@ class LiteLLM_OrganizationTable(LiteLLMPydanticObjectBase): organization_id: Optional[str] = None organization_alias: Optional[str] = None budget_id: str + spend: float = 0.0 metadata: Optional[dict] = None models: List[str] created_by: str @@ -1494,8 +1495,13 @@ class LiteLLM_OrganizationTable(LiteLLMPydanticObjectBase): class LiteLLM_OrganizationTableWithMembers(LiteLLM_OrganizationTable): - members: List[LiteLLM_OrganizationMembershipTable] - teams: List[LiteLLM_TeamTable] + """Returned by the /organization/info endpoint and /organization/list endpoint""" + + members: List[LiteLLM_OrganizationMembershipTable] = [] + teams: List[LiteLLM_TeamTable] = [] + litellm_budget_table: Optional[LiteLLM_BudgetTable] = None + created_at: datetime + updated_at: datetime class NewOrganizationResponse(LiteLLM_OrganizationTable): diff --git a/litellm/proxy/management_endpoints/organization_endpoints.py b/litellm/proxy/management_endpoints/organization_endpoints.py index c6bd51c0e7..20ba084d30 100644 --- a/litellm/proxy/management_endpoints/organization_endpoints.py +++ b/litellm/proxy/management_endpoints/organization_endpoints.py @@ -16,6 +16,7 @@ from typing import List, Optional, Tuple from fastapi import APIRouter, Depends, HTTPException, Request, status +from litellm._logging import verbose_proxy_logger from litellm.proxy._types import * from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.proxy.management_helpers.utils import ( @@ -250,14 +251,46 @@ async def list_organization( return response +@router.get( + "/organization/info", + tags=["organization management"], + dependencies=[Depends(user_api_key_auth)], + response_model=LiteLLM_OrganizationTableWithMembers, +) +async def info_organization(organization_id: str): + """ + Get the org specific information + """ + from litellm.proxy.proxy_server import prisma_client + + if prisma_client is None: + raise HTTPException(status_code=500, detail={"error": "No db connected"}) + + response: Optional[LiteLLM_OrganizationTableWithMembers] = ( + await prisma_client.db.litellm_organizationtable.find_unique( + where={"organization_id": organization_id}, + include={"litellm_budget_table": True, "members": True, "teams": True}, + ) + ) + + if response is None: + raise HTTPException(status_code=404, detail={"error": "Organization not found"}) + + response_pydantic_obj = LiteLLM_OrganizationTableWithMembers( + **response.model_dump() + ) + + return response_pydantic_obj + + @router.post( "/organization/info", tags=["organization management"], dependencies=[Depends(user_api_key_auth)], ) -async def info_organization(data: OrganizationRequest): +async def deprecated_info_organization(data: OrganizationRequest): """ - Get the org specific information + DEPRECATED: Use GET /organization/info instead """ from litellm.proxy.proxy_server import prisma_client @@ -378,6 +411,7 @@ async def organization_member_add( updated_organization_memberships=updated_organization_memberships, ) except Exception as e: + verbose_proxy_logger.exception(f"Error adding member to organization: {e}") if isinstance(e, HTTPException): raise ProxyException( message=getattr(e, "detail", f"Authentication Error({str(e)})"), @@ -418,12 +452,17 @@ async def add_member_to_organization( where={"user_id": member.user_id} ) - if member.user_email is not None: - existing_user_email_row = ( - await prisma_client.db.litellm_usertable.find_unique( - where={"user_email": member.user_email} + if existing_user_id_row is None and member.user_email is not None: + try: + existing_user_email_row = ( + await prisma_client.db.litellm_usertable.find_unique( + where={"user_email": member.user_email} + ) + ) + except Exception as e: + raise ValueError( + f"Potential NON-Existent or Duplicate user email in DB: Error finding a unique instance of user_email={member.user_email} in LiteLLM_UserTable.: {e}" ) - ) ## If user does not exist, create a new user if existing_user_id_row is None and existing_user_email_row is None: @@ -477,4 +516,9 @@ async def add_member_to_organization( return user_object, organization_membership except Exception as e: - raise ValueError(f"Error adding member to organization: {e}") + import traceback + + traceback.print_exc() + raise ValueError( + f"Error adding member={member} to organization={organization_id}: {e}" + ) diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index d9b5a0be91..2abe45b94f 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -27,6 +27,8 @@ import CacheDashboard from "@/components/cache_dashboard"; import { setGlobalLitellmHeaderName } from "@/components/networking"; import { Organization } from "@/components/networking"; import GuardrailsPanel from "@/components/guardrails"; +import { fetchUserModels } from "@/components/create_key_button"; + function getCookie(name: string) { const cookieValue = document.cookie .split("; ") @@ -81,6 +83,7 @@ export default function CreateKeyPage() { const [keys, setKeys] = useState(null); const [currentOrg, setCurrentOrg] = useState(defaultOrg); const [organizations, setOrganizations] = useState([]); + const [userModels, setUserModels] = useState([]); const [proxySettings, setProxySettings] = useState({ PROXY_BASE_URL: "", PROXY_LOGOUT_URL: "", @@ -171,6 +174,12 @@ export default function CreateKeyPage() { } } }, [token]); + + useEffect(() => { + if (accessToken && userID && userRole) { + fetchUserModels(userID, userRole, accessToken, setUserModels); + } + }, [accessToken, userID, userRole]); const handleOrgChange = (org: Organization) => { setCurrentOrg(org); @@ -283,11 +292,10 @@ export default function CreateKeyPage() { /> ) : page == "organizations" ? ( diff --git a/ui/litellm-dashboard/src/components/common_components/all_view.tsx b/ui/litellm-dashboard/src/components/common_components/all_view.tsx index 7ed32c7ed2..48f48dd9f4 100644 --- a/ui/litellm-dashboard/src/components/common_components/all_view.tsx +++ b/ui/litellm-dashboard/src/components/common_components/all_view.tsx @@ -35,6 +35,7 @@ interface DeleteModalProps { onCancel: () => void; title: string; message: string; + } interface DataTableProps { @@ -43,6 +44,7 @@ interface DataTableProps { actions?: Action[]; emptyMessage?: string; deleteModal?: DeleteModalProps; + onItemClick?: (item: any) => void; } const DataTable: React.FC = ({ @@ -50,7 +52,8 @@ const DataTable: React.FC = ({ columns, actions, emptyMessage = "No data available", - deleteModal + deleteModal, + onItemClick }) => { const renderCell = (column: Column, row: any) => { const value = row[column.accessor]; diff --git a/ui/litellm-dashboard/src/components/common_components/user_search_modal.tsx b/ui/litellm-dashboard/src/components/common_components/user_search_modal.tsx index e415c04f2b..aafd1ad7e5 100644 --- a/ui/litellm-dashboard/src/components/common_components/user_search_modal.tsx +++ b/ui/litellm-dashboard/src/components/common_components/user_search_modal.tsx @@ -1,8 +1,8 @@ import { useState, useCallback } from 'react'; -import { Modal, Form, Button, Select } from 'antd'; +import { Modal, Form, Button, Select, Tooltip } from 'antd'; import debounce from 'lodash/debounce'; import { userFilterUICall } from "@/components/networking"; - +import { InfoCircleOutlined } from '@ant-design/icons'; interface User { user_id: string; user_email: string; @@ -15,10 +15,17 @@ interface UserOption { user: User; } +interface Role { + label: string; + value: string; + description: string; +} + + interface FormValues { user_email: string; user_id: string; - role: 'admin' | 'user'; + role: string; } interface UserSearchModalProps { @@ -26,13 +33,22 @@ interface UserSearchModalProps { onCancel: () => void; onSubmit: (values: FormValues) => void; accessToken: string | null; + title?: string; + roles?: Role[]; + defaultRole?: string; } const UserSearchModal: React.FC = ({ isVisible, onCancel, onSubmit, - accessToken + accessToken, + title = "Add Team Member", + roles = [ + { label: "admin", value: "admin", description: "Admin role. Can create team keys, add members, and manage settings." }, + { label: "user", value: "user", description: "User role. Can view team info, but not manage it." } + ], + defaultRole = "user" }) => { const [form] = Form.useForm(); const [userOptions, setUserOptions] = useState([]); @@ -97,7 +113,7 @@ const UserSearchModal: React.FC = ({ return ( = ({ wrapperCol={{ span: 16 }} labelAlign="left" initialValues={{ - role: "user", + role: defaultRole, }} > = ({ name="role" className="mb-4" > - + {roles.map(role => ( + + + {role.label} + - {role.description} + + + ))} diff --git a/ui/litellm-dashboard/src/components/create_key_button.tsx b/ui/litellm-dashboard/src/components/create_key_button.tsx index 2de11c14c8..387e83d9f2 100644 --- a/ui/litellm-dashboard/src/components/create_key_button.tsx +++ b/ui/litellm-dashboard/src/components/create_key_button.tsx @@ -94,6 +94,29 @@ export const getTeamModels = (team: Team | null, allAvailableModels: string[]): return unfurlWildcardModelsInList(tempModelsToPick, allAvailableModels); }; +export const fetchUserModels = async (userID: string, userRole: string, accessToken: string, setUserModels: (models: string[]) => void) => { + try { + if (userID === null || userRole === null) { + return; + } + + if (accessToken !== null) { + const model_available = await modelAvailableCall( + accessToken, + userID, + userRole + ); + let available_model_names = model_available["data"].map( + (element: { id: string }) => element.id + ); + console.log("available_model_names:", available_model_names); + setUserModels(available_model_names); + } + } catch (error) { + console.error("Error fetching user models:", error); + } +}; + const CreateKey: React.FC = ({ userID, team, @@ -126,30 +149,9 @@ const CreateKey: React.FC = ({ }; useEffect(() => { - const fetchUserModels = async () => { - try { - if (userID === null || userRole === null) { - return; - } - - if (accessToken !== null) { - const model_available = await modelAvailableCall( - accessToken, - userID, - userRole - ); - let available_model_names = model_available["data"].map( - (element: { id: string }) => element.id - ); - console.log("available_model_names:", available_model_names); - setUserModels(available_model_names); - } - } catch (error) { - console.error("Error fetching user models:", error); - } - }; - - fetchUserModels(); + if (userID && userRole && accessToken) { + fetchUserModels(userID, userRole, accessToken, setUserModels); + } }, [accessToken, userID, userRole]); useEffect(() => { diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index fa4f17b497..97f6e17943 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -17,6 +17,7 @@ export interface Model { model_info: Object | null; } + export interface Organization { organization_id: string | null; organization_alias: string; @@ -834,6 +835,40 @@ export const organizationListCall = async (accessToken: String) => { } }; +export const organizationInfoCall = async ( + accessToken: String, + organizationID: String +) => { + try { + let url = proxyBaseUrl ? `${proxyBaseUrl}/organization/info` : `/organization/info`; + if (organizationID) { + url = `${url}?organization_id=${organizationID}`; + } + console.log("in teamInfoCall"); + const response = await fetch(url, { + method: "GET", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.text(); + handleError(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 const organizationCreateCall = async ( accessToken: string, formValues: Record // Assuming formValues is an object diff --git a/ui/litellm-dashboard/src/components/organization/organization_view.tsx b/ui/litellm-dashboard/src/components/organization/organization_view.tsx new file mode 100644 index 0000000000..7c2994bb1f --- /dev/null +++ b/ui/litellm-dashboard/src/components/organization/organization_view.tsx @@ -0,0 +1,410 @@ +import React, { useState, useEffect } from "react"; +import { + Card, + Title, + Text, + Tab, + TabList, + TabGroup, + TabPanel, + TabPanels, + Grid, + Badge, + Table, + TableHead, + TableRow, + TableHeaderCell, + TableBody, + TableCell, + Button as TremorButton, + Icon +} from "@tremor/react"; +import { Button, Form, Input, Select, message, InputNumber, Tooltip } from "antd"; +import { InfoCircleOutlined } from '@ant-design/icons'; +import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline"; +import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key"; +import { Member, Organization, organizationInfoCall, organizationMemberAddCall } from "../networking"; +import UserSearchModal from "../common_components/user_search_modal"; + + +interface OrganizationInfoProps { + organizationId: string; + onClose: () => void; + accessToken: string | null; + is_org_admin: boolean; + is_proxy_admin: boolean; + userModels: string[]; + editOrg: boolean; +} + +const OrganizationInfoView: React.FC = ({ + organizationId, + onClose, + accessToken, + is_org_admin, + is_proxy_admin, + userModels, + editOrg +}) => { + const [orgData, setOrgData] = useState(null); + const [loading, setLoading] = useState(true); + const [form] = Form.useForm(); + const [isEditing, setIsEditing] = useState(false); + const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false); + + const canEditOrg = is_org_admin || is_proxy_admin; + + const fetchOrgInfo = async () => { + try { + setLoading(true); + if (!accessToken) return; + const response = await organizationInfoCall(accessToken, organizationId); + setOrgData(response); + } catch (error) { + message.error("Failed to load organization information"); + console.error("Error fetching organization info:", error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchOrgInfo(); + }, [organizationId, accessToken]); + + const handleMemberAdd = async (values: any) => { + try { + if (accessToken == null) { + return; + } + + const member: Member = { + user_email: values.user_email, + user_id: values.user_id, + role: values.role, + } + const response = await organizationMemberAddCall(accessToken, organizationId, member); + + message.success("Organization member added successfully"); + setIsAddMemberModalVisible(false); + form.resetFields(); + fetchOrgInfo(); + } catch (error) { + message.error("Failed to add organization member"); + console.error("Error adding organization member:", error); + } + }; + + const handleOrgUpdate = async (values: any) => { + try { + if (!accessToken) return; + + const updateData = { + organization_id: organizationId, + organization_alias: values.organization_alias, + models: values.models, + litellm_budget_table: { + tpm_limit: values.tpm_limit, + rpm_limit: values.rpm_limit, + max_budget: values.max_budget, + budget_duration: values.budget_duration, + } + }; + + const response = await fetch('/organization/update', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(updateData), + }); + + if (!response.ok) throw new Error('Failed to update organization'); + + message.success("Organization settings updated successfully"); + setIsEditing(false); + fetchOrgInfo(); + } catch (error) { + message.error("Failed to update organization settings"); + console.error("Error updating organization:", error); + } + }; + + if (loading) { + return
Loading...
; + } + + if (!orgData) { + return
Organization not found
; + } + + return ( +
+
+
+ + {orgData.organization_alias} + {orgData.organization_id} +
+
+ + + + Overview + Members + Settings + + + + {/* Overview Panel */} + + + + Organization Details +
+ Created: {new Date(orgData.created_at).toLocaleDateString()} + Updated: {new Date(orgData.updated_at).toLocaleDateString()} + Created By: {orgData.created_by} +
+
+ + + Budget Status +
+ ${orgData.spend.toFixed(6)} + of {orgData.litellm_budget_table.max_budget === null ? "Unlimited" : `$${orgData.litellm_budget_table.max_budget}`} + {orgData.litellm_budget_table.budget_duration && ( + Reset: {orgData.litellm_budget_table.budget_duration} + )} +
+
+ + + Rate Limits +
+ TPM: {orgData.litellm_budget_table.tpm_limit || 'Unlimited'} + RPM: {orgData.litellm_budget_table.rpm_limit || 'Unlimited'} + {orgData.litellm_budget_table.max_parallel_requests && ( + Max Parallel Requests: {orgData.litellm_budget_table.max_parallel_requests} + )} +
+
+ + + Models +
+ {orgData.models.map((model, index) => ( + + {model} + + ))} +
+
+
+
+ + {/* Budget Panel */} + +
+ + + + + User ID + Role + Spend + Created At + + + + + + {orgData.members?.map((member, index) => ( + + + {member.user_id} + + + {member.user_role} + + + ${member.spend.toFixed(6)} + + + {new Date(member.created_at).toLocaleString()} + + + {canEditOrg && ( + <> + { + // TODO: Implement edit member functionality + }} + /> + { + // TODO: Implement delete member functionality + }} + /> + + )} + + + ))} + +
+
+ {canEditOrg && ( + { + setIsAddMemberModalVisible(true); + }}> + Add Member + + )} +
+
+ + {/* Settings Panel */} + + +
+ Organization Settings + {(canEditOrg && !isEditing) && ( + setIsEditing(true)} + > + Edit Settings + + )} +
+ + {isEditing ? ( +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + Save Changes + +
+
+ ) : ( +
+
+ Organization Name +
{orgData.organization_alias}
+
+
+ Organization ID +
{orgData.organization_id}
+
+
+ Created At +
{new Date(orgData.created_at).toLocaleString()}
+
+
+ Models +
+ {orgData.models.map((model, index) => ( + + {model} + + ))} +
+
+
+ Rate Limits +
TPM: {orgData.litellm_budget_table.tpm_limit || 'Unlimited'}
+
RPM: {orgData.litellm_budget_table.rpm_limit || 'Unlimited'}
+
+
+ Budget +
Max: {orgData.litellm_budget_table.max_budget !== null ? `$${orgData.litellm_budget_table.max_budget}` : 'No Limit'}
+
Reset: {orgData.litellm_budget_table.budget_duration || 'Never'}
+
+
+ )} +
+
+
+
+ setIsAddMemberModalVisible(false)} + onSubmit={handleMemberAdd} + accessToken={accessToken} + title="Add Organization Member" + roles={[ + { label: "org_admin", value: "org_admin", description: "Can add and remove members, and change their roles." }, + { label: "internal_user", value: "internal_user", description: "Can view/create keys for themselves within organization." }, + { label: "internal_user_viewer", value: "internal_user_viewer", description: "Can only view their keys within organization." } + ]} + defaultRole="internal_user" + /> +
+ ); +}; + +export default OrganizationInfoView; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/organizations.tsx b/ui/litellm-dashboard/src/components/organizations.tsx index 87ec9d0c2a..58df99dba3 100644 --- a/ui/litellm-dashboard/src/components/organizations.tsx +++ b/ui/litellm-dashboard/src/components/organizations.tsx @@ -1,282 +1,392 @@ -import React, { useState, useEffect } from "react"; -import { organizationListCall, organizationMemberAddCall, Member, modelAvailableCall } from "./networking"; -import { Team } from "./key_team_helpers/key_list"; +import React, { useState, useEffect } from 'react'; import { - Col, + Table, + TableHead, + TableHeaderCell, + TableBody, + TableRow, + TableCell, + Card, + Text, + Badge, + Icon, Grid, - Text + Col, + Button, + TabGroup, + TabList, + Tab, + TabPanels, + TabPanel, } from "@tremor/react"; -import OrganizationForm from "@/components/organization/add_org"; -import AddOrgAdmin from "@/components/organization/add_org_admin"; -import MemberListTable from "@/components/organization/view_members_of_org"; -import { Select, SelectItem } from "@tremor/react"; -const isLocal = process.env.NODE_ENV === "development"; -const proxyBaseUrl = isLocal ? "http://localhost:4000" : null; -if (isLocal != true) { - console.log = function() {}; -} -interface TeamProps { - teams: Team[] | null; - searchParams: any; +import { Modal, Form, InputNumber, Tooltip, Select as Select2 } from "antd"; +import { InfoCircleOutlined } from '@ant-design/icons'; +import { PencilAltIcon, TrashIcon, RefreshIcon } from "@heroicons/react/outline"; +import { TextInput } from "@tremor/react"; +import { getModelDisplayName } from './key_team_helpers/fetch_available_models_team_key'; +import OrganizationInfoView from './organization/organization_view'; +import { Organization, organizationListCall, organizationCreateCall } from './networking'; +interface OrganizationsTableProps { + organizations: Organization[]; + userRole: string; + userModels: string[]; accessToken: string | null; - setTeams: React.Dispatch>; - userID: string | null; - userRole: string | null; + lastRefreshed?: string; + handleRefreshClick?: () => void; + currentOrg?: any; + guardrailsList?: string[]; + setOrganizations: (organizations: Organization[]) => void; premiumUser: boolean; } - -import DataTable from "@/components/common_components/all_view"; -import { Action } from "@/components/common_components/all_view"; -import { Typography, message } from "antd"; -import { Organization, EditModalProps } from "@/components/organization/types"; - -interface OrganizationsTableProps { - organizations: Organization[]; - userRole?: string; - onEdit?: (organization: Organization) => void; - onDelete?: (organization: Organization) => void; - isDeleteModalOpen: boolean; - setIsDeleteModalOpen: (value: boolean) => void; - selectedOrganization: Organization | null; - setSelectedOrganization: (value: Organization | null) => void; -} - - -const EditOrganizationModal: React.FC = ({ - visible, - onCancel, - entity, - onSubmit -}) => { - return
; +const fetchOrganizations = async (accessToken: string, setOrganizations: (organizations: Organization[]) => void) => { + const organizations = await organizationListCall(accessToken); + setOrganizations(organizations); }; - -// Inside your Teams component const OrganizationsTable: React.FC = ({ organizations, userRole, - onEdit, - onDelete, - isDeleteModalOpen, - setIsDeleteModalOpen, - selectedOrganization, - setSelectedOrganization -}) => { - - const columns = [ - { - header: "Organization Name", - accessor: "organization_alias", - width: "4px", - style: { - whiteSpace: "pre-wrap", - overflow: "hidden" - } - }, - { - header: "Organization ID", - accessor: "organization_id", - width: "4px", - style: { - whiteSpace: "nowrap", - overflow: "hidden", - textOverflow: "ellipsis", - fontSize: "0.75em" - } - }, - { - header: "Spend (USD)", - accessor: "spend" - }, - { - header: "Budget (USD)", - accessor: "max_budget", - cellRenderer: (value: number | null) => - value !== null && value !== undefined ? value : "No limit" - }, - { - header: "Models", - accessor: "models" - }, - { - header: "TPM / RPM Limits", - accessor: "limits", - cellRenderer: (value: any, row: Organization) => ( -
- TPM: {row.tpm_limit ? row.tpm_limit : "Unlimited"} -
- RPM: {row.rpm_limit ? row.rpm_limit : "Unlimited"} -
- ) - }, - { - header: "Info", - accessor: "info", - cellRenderer: (value: any, row: Organization) => ( -
-
- {row.members?.length || 0} Members -
-
- ) - } - ]; - - const actions: Action[] = [ - ...(onEdit && userRole === "Admin" ? [{ - icon: undefined, // Replace with your PencilAltIcon - onClick: (org: Organization) => onEdit(org), - condition: () => userRole === "Admin", - tooltip: "Edit organization" - }] : []), - ...(onDelete && userRole === "Admin" ? [{ - icon: undefined, // Replace with your TrashIcon - onClick: (org: Organization) => onDelete(org), - condition: () => userRole === "Admin", - tooltip: "Delete organization" - }] : []) - ]; - - return ( - { - if (selectedOrganization && onDelete) { - onDelete(selectedOrganization); - } - setIsDeleteModalOpen(false); - setSelectedOrganization(null); - }, - onCancel: () => { - setIsDeleteModalOpen(false); - setSelectedOrganization(null); - }, - title: "Delete Organization", - message: "Are you sure you want to delete this organization?" - }} - /> - ); -}; - -const Organizations: React.FC = ({ + userModels, accessToken, - userID, - userRole, + lastRefreshed, + handleRefreshClick, + currentOrg, + guardrailsList = [], + setOrganizations, premiumUser }) => { - const [organizations, setOrganizations] = useState([]); - const { Title, Paragraph } = Typography; + const [selectedOrgId, setSelectedOrgId] = useState(null); + const [editOrg, setEditOrg] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [selectedOrganization, setSelectedOrganization] = useState(null); - const [userModels, setUserModels] = useState([]); + const [orgToDelete, setOrgToDelete] = useState(null); + const [isOrgModalVisible, setIsOrgModalVisible] = useState(false); + const [form] = Form.useForm(); useEffect(() => { - if (!accessToken || !userID || !userRole) return; - - const fetchUserModels = async () => { - try { - const model_available = await modelAvailableCall( - accessToken, - userID, - userRole - ); - let available_model_names = model_available["data"].map( - (element: { id: string }) => element.id - ); - console.log("available_model_names:", available_model_names); - setUserModels(available_model_names); - } catch (error) { - console.error("Error fetching user models:", error); - } - }; - - const fetchData = async () => { - let givenOrganizations; - givenOrganizations = await organizationListCall(accessToken) - console.log(`givenOrganizations: ${givenOrganizations}`) - setOrganizations(givenOrganizations) - sessionStorage.setItem('organizations', JSON.stringify(givenOrganizations)); + if (organizations.length === 0 && accessToken) { + fetchOrganizations(accessToken, setOrganizations); } - if (premiumUser) { - fetchUserModels() - fetchData() - } - }, [accessToken]); + }, [organizations, accessToken]); + + const handleDelete = (orgId: string | null) => { + if (!orgId) return; + + setOrgToDelete(orgId); + setIsDeleteModalOpen(true); + }; + + const confirmDelete = async () => { + if (!orgToDelete || !accessToken) return; - const handleMemberCreate = async (formValues: Record) => { - if (!selectedOrganization || !accessToken) return; try { - let member: Member = { - user_email: formValues.user_email, - user_id: formValues.user_id, - role: formValues.role - } - await organizationMemberAddCall( - accessToken, - selectedOrganization["organization_id"], - member - ); - message.success("Member added"); + const response = await fetch(`/organization/delete?organization_id=${orgToDelete}`, { + method: 'DELETE', + headers: { + 'Authorization': `Bearer ${accessToken}` + } + }); + + if (!response.ok) throw new Error('Failed to delete organization'); + + setIsDeleteModalOpen(false); + setOrgToDelete(null); + // Refresh organizations list } catch (error) { - console.error("Error creating the team:", error); - message.error("Error creating the organization: " + error); + console.error('Error deleting organization:', error); } -}; + }; + + const cancelDelete = () => { + setIsDeleteModalOpen(false); + setOrgToDelete(null); + }; + + const handleCreate = async (values: any) => { + try { + if (!accessToken) return; + + await organizationCreateCall(accessToken, values); + setIsOrgModalVisible(false); + form.resetFields(); + // Refresh organizations list + fetchOrganizations(accessToken, setOrganizations); + } catch (error) { + console.error('Error creating organization:', error); + } + }; + + const handleCancel = () => { + setIsOrgModalVisible(false); + form.resetFields(); + }; + + if (!premiumUser) { + return ( +
+ This is a LiteLLM Enterprise feature, and requires a valid key to use. Get a trial key here. +
+ ); + } + + if (selectedOrgId) { + return ( + { + setSelectedOrgId(null); + setEditOrg(false); + }} + accessToken={accessToken} + is_org_admin={true} // You'll need to implement proper org admin check + is_proxy_admin={userRole === "Admin"} + userModels={userModels} + editOrg={editOrg} + /> + ); + } return ( -
- - - ✨ All Organizations - This is a LiteLLM Enterprise feature, and requires a valid key to use. Get a trial key here - {userRole ? OrganizationsTable({organizations, userRole, isDeleteModalOpen, setIsDeleteModalOpen, selectedOrganization, setSelectedOrganization}) : null} - - {userRole == "Admin" && accessToken && premiumUser ? : null} - {premiumUser ? - - Organization Members - - If you belong to multiple organizations, this setting controls which organizations' - members you see. - + + +
+ Your Organizations +
+
+ {lastRefreshed && Last Refreshed: {lastRefreshed}} + +
+
+ + + + Click on “Organization ID” to view organization details. + + + + + + + + Organization ID + Organization Name + Created + Spend (USD) + Budget (USD) + Models + TPM / RPM Limits + Info + Actions + + - {organizations && organizations.length > 0 ? ( -
+
+ + {(userRole === "Admin" || userRole === "Org Admin") && ( + +
+ + Create New Organization + + +
+ + + + + + + All Proxy Models + + {userModels && userModels.length > 0 && userModels.map((model) => ( + + {getModelDisplayName(model)} + + ))} + + + + + + + + + daily + weekly + monthly + + + + + + + + + +
+ +
+
+
+ + )} + + + + + {isDeleteModalOpen ? ( +
+
+ + + + +
+
+
+
+

+ Delete Organization +

+
+

+ Are you sure you want to delete this organization? +

+
+
+
+
+
+ + +
+
+
+
+ ) : <>} + ); }; -export default Organizations; +export default OrganizationsTable; \ No newline at end of file