Spaces:
Sleeping
Sleeping
import React, { useState, useEffect } from "react"; | |
import { | |
Card, | |
Title, | |
Text, | |
TextInput, | |
Tab, | |
TabList, | |
TabGroup, | |
TabPanel, | |
TabPanels, | |
Grid, | |
Badge, | |
Table, | |
TableHead, | |
TableRow, | |
TableHeaderCell, | |
TableBody, | |
TableCell, | |
Button as TremorButton, | |
Icon | |
} from "@tremor/react"; | |
import NumericalInput from "../shared/numerical_input"; | |
import { Button, Form, Input, Select, message, 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, organizationMemberUpdateCall, organizationMemberDeleteCall, organizationUpdateCall } from "../networking"; | |
import UserSearchModal from "../common_components/user_search_modal"; | |
import MemberModal from "../team/edit_membership"; | |
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<OrganizationInfoProps> = ({ | |
organizationId, | |
onClose, | |
accessToken, | |
is_org_admin, | |
is_proxy_admin, | |
userModels, | |
editOrg | |
}) => { | |
const [orgData, setOrgData] = useState<Organization | null>(null); | |
const [loading, setLoading] = useState(true); | |
const [form] = Form.useForm(); | |
const [isEditing, setIsEditing] = useState(false); | |
const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false); | |
const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false); | |
const [selectedEditMember, setSelectedEditMember] = useState<Member | null>(null); | |
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 handleMemberUpdate = async (values: any) => { | |
try { | |
if (!accessToken) return; | |
const member: Member = { | |
user_email: values.user_email, | |
user_id: values.user_id, | |
role: values.role, | |
} | |
const response = await organizationMemberUpdateCall(accessToken, organizationId, member); | |
message.success("Organization member updated successfully"); | |
setIsEditMemberModalVisible(false); | |
form.resetFields(); | |
fetchOrgInfo(); | |
} catch (error) { | |
message.error("Failed to update organization member"); | |
console.error("Error updating organization member:", error); | |
} | |
}; | |
const handleMemberDelete = async (values: any) => { | |
try { | |
if (!accessToken) return; | |
await organizationMemberDeleteCall(accessToken, organizationId, values.user_id); | |
message.success("Organization member deleted successfully"); | |
setIsEditMemberModalVisible(false); | |
form.resetFields(); | |
fetchOrgInfo(); | |
} catch (error) { | |
message.error("Failed to delete organization member"); | |
console.error("Error deleting 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, | |
}, | |
metadata: values.metadata ? JSON.parse(values.metadata) : null, | |
}; | |
const response = await organizationUpdateCall(accessToken, updateData); | |
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 <div className="p-4">Loading...</div>; | |
} | |
if (!orgData) { | |
return <div className="p-4">Organization not found</div>; | |
} | |
return ( | |
<div className="w-full h-screen p-4 bg-white"> | |
<div className="flex justify-between items-center mb-6"> | |
<div> | |
<Button onClick={onClose} className="mb-4">← Back</Button> | |
<Title>{orgData.organization_alias}</Title> | |
<Text className="text-gray-500 font-mono">{orgData.organization_id}</Text> | |
</div> | |
</div> | |
<TabGroup defaultIndex={editOrg ? 2 : 0}> | |
<TabList className="mb-4"> | |
<Tab>Overview</Tab> | |
<Tab>Members</Tab> | |
<Tab>Settings</Tab> | |
</TabList> | |
<TabPanels> | |
{/* Overview Panel */} | |
<TabPanel> | |
<Grid numItems={1} numItemsSm={2} numItemsLg={3} className="gap-6"> | |
<Card> | |
<Text>Organization Details</Text> | |
<div className="mt-2"> | |
<Text>Created: {new Date(orgData.created_at).toLocaleDateString()}</Text> | |
<Text>Updated: {new Date(orgData.updated_at).toLocaleDateString()}</Text> | |
<Text>Created By: {orgData.created_by}</Text> | |
</div> | |
</Card> | |
<Card> | |
<Text>Budget Status</Text> | |
<div className="mt-2"> | |
<Title>${orgData.spend.toFixed(6)}</Title> | |
<Text>of {orgData.litellm_budget_table.max_budget === null ? "Unlimited" : `$${orgData.litellm_budget_table.max_budget}`}</Text> | |
{orgData.litellm_budget_table.budget_duration && ( | |
<Text className="text-gray-500">Reset: {orgData.litellm_budget_table.budget_duration}</Text> | |
)} | |
</div> | |
</Card> | |
<Card> | |
<Text>Rate Limits</Text> | |
<div className="mt-2"> | |
<Text>TPM: {orgData.litellm_budget_table.tpm_limit || 'Unlimited'}</Text> | |
<Text>RPM: {orgData.litellm_budget_table.rpm_limit || 'Unlimited'}</Text> | |
{orgData.litellm_budget_table.max_parallel_requests && ( | |
<Text>Max Parallel Requests: {orgData.litellm_budget_table.max_parallel_requests}</Text> | |
)} | |
</div> | |
</Card> | |
<Card> | |
<Text>Models</Text> | |
<div className="mt-2 flex flex-wrap gap-2"> | |
{orgData.models.map((model, index) => ( | |
<Badge key={index} color="red"> | |
{model} | |
</Badge> | |
))} | |
</div> | |
</Card> | |
</Grid> | |
</TabPanel> | |
{/* Budget Panel */} | |
<TabPanel> | |
<div className="space-y-4"> | |
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[75vh]"> | |
<Table> | |
<TableHead> | |
<TableRow> | |
<TableHeaderCell>User ID</TableHeaderCell> | |
<TableHeaderCell>Role</TableHeaderCell> | |
<TableHeaderCell>Spend</TableHeaderCell> | |
<TableHeaderCell>Created At</TableHeaderCell> | |
<TableHeaderCell></TableHeaderCell> | |
</TableRow> | |
</TableHead> | |
<TableBody> | |
{orgData.members?.map((member, index) => ( | |
<TableRow key={index}> | |
<TableCell> | |
<Text className="font-mono">{member.user_id}</Text> | |
</TableCell> | |
<TableCell> | |
<Text className="font-mono">{member.user_role}</Text> | |
</TableCell> | |
<TableCell> | |
<Text>${member.spend.toFixed(6)}</Text> | |
</TableCell> | |
<TableCell> | |
<Text>{new Date(member.created_at).toLocaleString()}</Text> | |
</TableCell> | |
<TableCell> | |
{canEditOrg && ( | |
<> | |
<Icon | |
icon={PencilAltIcon} | |
size="sm" | |
onClick={() => { | |
setSelectedEditMember({ | |
"role": member.user_role, | |
"user_email": member.user_email, | |
"user_id": member.user_id | |
}); | |
setIsEditMemberModalVisible(true); | |
}} | |
/> | |
<Icon | |
icon={TrashIcon} | |
size="sm" | |
onClick={() => { | |
handleMemberDelete(member); | |
}} | |
/> | |
</> | |
)} | |
</TableCell> | |
</TableRow> | |
))} | |
</TableBody> | |
</Table> | |
</Card> | |
{canEditOrg && ( | |
<TremorButton onClick={() => { | |
setIsAddMemberModalVisible(true); | |
}}> | |
Add Member | |
</TremorButton> | |
)} | |
</div> | |
</TabPanel> | |
{/* Settings Panel */} | |
<TabPanel> | |
<Card> | |
<div className="flex justify-between items-center mb-4"> | |
<Title>Organization Settings</Title> | |
{(canEditOrg && !isEditing) && ( | |
<TremorButton | |
onClick={() => setIsEditing(true)} | |
> | |
Edit Settings | |
</TremorButton> | |
)} | |
</div> | |
{isEditing ? ( | |
<Form | |
form={form} | |
onFinish={handleOrgUpdate} | |
initialValues={{ | |
organization_alias: orgData.organization_alias, | |
models: orgData.models, | |
tpm_limit: orgData.litellm_budget_table.tpm_limit, | |
rpm_limit: orgData.litellm_budget_table.rpm_limit, | |
max_budget: orgData.litellm_budget_table.max_budget, | |
budget_duration: orgData.litellm_budget_table.budget_duration, | |
metadata: orgData.metadata ? JSON.stringify(orgData.metadata, null, 2) : "", | |
}} | |
layout="vertical" | |
> | |
<Form.Item | |
label="Organization Name" | |
name="organization_alias" | |
rules={[{ required: true, message: "Please input an organization name" }]} | |
> | |
<TextInput /> | |
</Form.Item> | |
<Form.Item label="Models" name="models"> | |
<Select | |
mode="multiple" | |
placeholder="Select models" | |
> | |
<Select.Option key="all-proxy-models" value="all-proxy-models"> | |
All Proxy Models | |
</Select.Option> | |
{userModels.map((model) => ( | |
<Select.Option key={model} value={model}> | |
{getModelDisplayName(model)} | |
</Select.Option> | |
))} | |
</Select> | |
</Form.Item> | |
<Form.Item label="Max Budget (USD)" name="max_budget"> | |
<NumericalInput step={0.01} precision={2} style={{ width: "100%" }} /> | |
</Form.Item> | |
<Form.Item label="Reset Budget" name="budget_duration"> | |
<Select placeholder="n/a"> | |
<Select.Option value="24h">daily</Select.Option> | |
<Select.Option value="7d">weekly</Select.Option> | |
<Select.Option value="30d">monthly</Select.Option> | |
</Select> | |
</Form.Item> | |
<Form.Item label="Tokens per minute Limit (TPM)" name="tpm_limit"> | |
<NumericalInput step={1} style={{ width: "100%" }} /> | |
</Form.Item> | |
<Form.Item label="Requests per minute Limit (RPM)" name="rpm_limit"> | |
<NumericalInput step={1} style={{ width: "100%" }} /> | |
</Form.Item> | |
<Form.Item label="Metadata" name="metadata"> | |
<Input.TextArea rows={4} /> | |
</Form.Item> | |
<div className="flex justify-end gap-2 mt-6"> | |
<Button onClick={() => setIsEditing(false)}> | |
Cancel | |
</Button> | |
<TremorButton type="submit"> | |
Save Changes | |
</TremorButton> | |
</div> | |
</Form> | |
) : ( | |
<div className="space-y-4"> | |
<div> | |
<Text className="font-medium">Organization Name</Text> | |
<div>{orgData.organization_alias}</div> | |
</div> | |
<div> | |
<Text className="font-medium">Organization ID</Text> | |
<div className="font-mono">{orgData.organization_id}</div> | |
</div> | |
<div> | |
<Text className="font-medium">Created At</Text> | |
<div>{new Date(orgData.created_at).toLocaleString()}</div> | |
</div> | |
<div> | |
<Text className="font-medium">Models</Text> | |
<div className="flex flex-wrap gap-2 mt-1"> | |
{orgData.models.map((model, index) => ( | |
<Badge key={index} color="red"> | |
{model} | |
</Badge> | |
))} | |
</div> | |
</div> | |
<div> | |
<Text className="font-medium">Rate Limits</Text> | |
<div>TPM: {orgData.litellm_budget_table.tpm_limit || 'Unlimited'}</div> | |
<div>RPM: {orgData.litellm_budget_table.rpm_limit || 'Unlimited'}</div> | |
</div> | |
<div> | |
<Text className="font-medium">Budget</Text> | |
<div>Max: {orgData.litellm_budget_table.max_budget !== null ? `$${orgData.litellm_budget_table.max_budget}` : 'No Limit'}</div> | |
<div>Reset: {orgData.litellm_budget_table.budget_duration || 'Never'}</div> | |
</div> | |
</div> | |
)} | |
</Card> | |
</TabPanel> | |
</TabPanels> | |
</TabGroup> | |
<UserSearchModal | |
isVisible={isAddMemberModalVisible} | |
onCancel={() => 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" | |
/> | |
<MemberModal | |
visible={isEditMemberModalVisible} | |
onCancel={() => setIsEditMemberModalVisible(false)} | |
onSubmit={handleMemberUpdate} | |
initialData={selectedEditMember} | |
mode="edit" | |
config={{ | |
title: "Edit Member", | |
showEmail: true, | |
showUserId: true, | |
roleOptions: [ | |
{ label: "Org Admin", value: "org_admin" }, | |
{ label: "Internal User", value: "internal_user" }, | |
{ label: "Internal User Viewer", value: "internal_user_viewer" } | |
] | |
}} | |
/> | |
</div> | |
); | |
}; | |
export default OrganizationInfoView; |