|
import React, { useState, useEffect } from "react"; |
|
import { |
|
Card, |
|
Title, |
|
Text, |
|
Tab, |
|
TabList, |
|
TabGroup, |
|
TabPanel, |
|
TabPanels, |
|
Grid, |
|
Badge, |
|
Button as TremorButton, |
|
TableRow, |
|
TableCell, |
|
TableHead, |
|
TableHeaderCell, |
|
TableBody, |
|
Table, |
|
Icon |
|
} from "@tremor/react"; |
|
import { teamInfoCall, teamMemberDeleteCall, teamMemberAddCall, teamMemberUpdateCall, Member } from "@/components/networking"; |
|
import { Button, Modal, Form, Input, Select as AntSelect, message } from "antd"; |
|
import { PencilAltIcon, PlusIcon, TrashIcon } from "@heroicons/react/outline"; |
|
import TeamMemberModal from "./edit_membership"; |
|
import UserSearchModal from "@/components/common_components/user_search_modal"; |
|
interface TeamData { |
|
team_id: string; |
|
team_info: { |
|
team_alias: string; |
|
team_id: string; |
|
organization_id: string | null; |
|
admins: string[]; |
|
members: string[]; |
|
members_with_roles: Member[]; |
|
metadata: Record<string, any>; |
|
tpm_limit: number | null; |
|
rpm_limit: number | null; |
|
max_budget: number | null; |
|
budget_duration: string | null; |
|
models: string[]; |
|
blocked: boolean; |
|
spend: number; |
|
max_parallel_requests: number | null; |
|
budget_reset_at: string | null; |
|
model_id: string | null; |
|
litellm_model_table: string | null; |
|
created_at: string; |
|
}; |
|
keys: any[]; |
|
team_memberships: any[]; |
|
} |
|
|
|
interface TeamInfoProps { |
|
teamId: string; |
|
onClose: () => void; |
|
accessToken: string | null; |
|
is_team_admin: boolean; |
|
is_proxy_admin: boolean; |
|
} |
|
|
|
const TeamInfoView: React.FC<TeamInfoProps> = ({ |
|
teamId, |
|
onClose, |
|
accessToken, |
|
is_team_admin, |
|
is_proxy_admin |
|
}) => { |
|
const [teamData, setTeamData] = useState<TeamData | null>(null); |
|
const [loading, setLoading] = useState(true); |
|
const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false); |
|
const [form] = Form.useForm(); |
|
const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false); |
|
const [selectedEditMember, setSelectedEditMember] = useState<Member | null>(null); |
|
|
|
const canManageMembers = is_team_admin || is_proxy_admin; |
|
|
|
const fetchTeamInfo = async () => { |
|
try { |
|
setLoading(true); |
|
if (!accessToken) return; |
|
const response = await teamInfoCall(accessToken, teamId); |
|
setTeamData(response); |
|
} catch (error) { |
|
message.error("Failed to load team information"); |
|
console.error("Error fetching team info:", error); |
|
} finally { |
|
setLoading(false); |
|
} |
|
}; |
|
|
|
useEffect(() => { |
|
fetchTeamInfo(); |
|
}, [teamId, accessToken]); |
|
|
|
|
|
const handleMemberCreate = async (values: any) => { |
|
try { |
|
if (accessToken == null) { |
|
return; |
|
} |
|
|
|
const member: Member = { |
|
user_email: values.user_email, |
|
user_id: values.user_id, |
|
role: values.role, |
|
} |
|
const response = await teamMemberAddCall(accessToken, teamId, member); |
|
|
|
|
|
message.success("Team member added successfully"); |
|
setIsAddMemberModalVisible(false); |
|
form.resetFields(); |
|
fetchTeamInfo(); |
|
} catch (error) { |
|
message.error("Failed to add team member"); |
|
console.error("Error adding team member:", error); |
|
} |
|
}; |
|
|
|
const handleMemberDelete = async (member: Member) => { |
|
try { |
|
if (accessToken == null) { |
|
return; |
|
} |
|
|
|
const response = await teamMemberDeleteCall(accessToken, teamId, member); |
|
|
|
message.success("Team member removed successfully"); |
|
fetchTeamInfo(); |
|
} catch (error) { |
|
message.error("Failed to remove team member"); |
|
console.error("Error removing team member:", error); |
|
} |
|
}; |
|
|
|
const handleMemberUpdate = async (values: any) => { |
|
try { |
|
if (accessToken == null) { |
|
return; |
|
} |
|
|
|
const member: Member = { |
|
user_email: values.user_email, |
|
user_id: values.user_id, |
|
role: values.role, |
|
} |
|
|
|
const response = await teamMemberUpdateCall(accessToken, teamId, member); |
|
|
|
message.success("Team member updated successfully"); |
|
setIsEditMemberModalVisible(false); |
|
fetchTeamInfo(); |
|
} catch (error) { |
|
message.error("Failed to update team member"); |
|
console.error("Error updating team member:", error); |
|
} |
|
} |
|
|
|
if (loading) { |
|
return <div className="p-4">Loading...</div>; |
|
} |
|
|
|
if (!teamData?.team_info) { |
|
return <div className="p-4">Team not found</div>; |
|
} |
|
|
|
const { team_info: info } = teamData; |
|
|
|
const renderMembersPanel = () => ( |
|
<div className="space-y-4"> |
|
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh]"> |
|
<Table> |
|
<TableHead> |
|
<TableRow> |
|
<TableHeaderCell>User ID</TableHeaderCell> |
|
<TableHeaderCell>User Email</TableHeaderCell> |
|
<TableHeaderCell>Role</TableHeaderCell> |
|
</TableRow> |
|
</TableHead> |
|
|
|
<TableBody> |
|
{teamData |
|
? teamData.team_info.members_with_roles.map( |
|
(member: any, index: number) => ( |
|
<TableRow key={index}> |
|
<TableCell> |
|
<Text className="font-mono">{member["user_id"]}</Text> |
|
</TableCell> |
|
<TableCell> |
|
<Text className="font-mono">{member["user_email"]}</Text> |
|
</TableCell> |
|
<TableCell> |
|
<Text className="font-mono">{member["role"]}</Text> |
|
</TableCell> |
|
<TableCell> |
|
{is_team_admin ? ( |
|
<> |
|
<Icon |
|
icon={PencilAltIcon} |
|
size="sm" |
|
onClick={() => { |
|
setSelectedEditMember(member); |
|
setIsEditMemberModalVisible(true); |
|
}} |
|
/> |
|
<Icon |
|
onClick={() => {handleMemberDelete(member)}} |
|
icon={TrashIcon} |
|
size="sm" |
|
/> |
|
</> |
|
) : null} |
|
</TableCell> |
|
</TableRow> |
|
) |
|
) |
|
: null} |
|
</TableBody> |
|
</Table> |
|
</Card> |
|
<TremorButton onClick={() => setIsAddMemberModalVisible(true)}>Add Member</TremorButton> |
|
</div> |
|
); |
|
|
|
return ( |
|
<div className="p-4"> |
|
<div className="flex justify-between items-center mb-6"> |
|
<div> |
|
<Button onClick={onClose} className="mb-4">← Back</Button> |
|
<Title>{info.team_alias}</Title> |
|
<Text className="text-gray-500 font-mono">{info.team_id}</Text> |
|
</div> |
|
</div> |
|
|
|
<TabGroup> |
|
<TabList className="mb-4"> |
|
<Tab>Overview</Tab> |
|
<Tab>Members</Tab> |
|
<Tab>Settings</Tab> |
|
</TabList> |
|
|
|
<TabPanels> |
|
<TabPanel> |
|
<Grid numItems={1} numItemsSm={2} numItemsLg={3} className="gap-6"> |
|
<Card> |
|
<Text>Budget Status</Text> |
|
<div className="mt-2"> |
|
<Title>${info.spend.toFixed(6)}</Title> |
|
<Text>of {info.max_budget === null ? "Unlimited" : `$${info.max_budget}`}</Text> |
|
{info.budget_duration && ( |
|
<Text className="text-gray-500">Reset: {info.budget_duration}</Text> |
|
)} |
|
</div> |
|
</Card> |
|
|
|
<Card> |
|
<Text>Rate Limits</Text> |
|
<div className="mt-2"> |
|
<Text>TPM: {info.tpm_limit || 'Unlimited'}</Text> |
|
<Text>RPM: {info.rpm_limit || 'Unlimited'}</Text> |
|
{info.max_parallel_requests && ( |
|
<Text>Max Parallel Requests: {info.max_parallel_requests}</Text> |
|
)} |
|
</div> |
|
</Card> |
|
|
|
<Card> |
|
<Text>Models</Text> |
|
<div className="mt-2 flex flex-wrap gap-2"> |
|
{info.models.map((model, index) => ( |
|
<Badge key={index} color="red"> |
|
{model} |
|
</Badge> |
|
))} |
|
</div> |
|
</Card> |
|
</Grid> |
|
</TabPanel> |
|
|
|
<TabPanel> |
|
{renderMembersPanel()} |
|
</TabPanel> |
|
|
|
<TabPanel> |
|
<Card> |
|
<Title>Team Settings</Title> |
|
<div className="mt-4 space-y-4"> |
|
<div> |
|
<Text className="font-medium">Team ID</Text> |
|
<Text className="font-mono">{info.team_id}</Text> |
|
</div> |
|
<div> |
|
<Text className="font-medium">Created At</Text> |
|
<Text>{new Date(info.created_at).toLocaleString()}</Text> |
|
</div> |
|
<div> |
|
<Text className="font-medium">Status</Text> |
|
<Badge color={info.blocked ? 'red' : 'green'}> |
|
{info.blocked ? 'Blocked' : 'Active'} |
|
</Badge> |
|
</div> |
|
</div> |
|
</Card> |
|
<TeamMemberModal |
|
visible={isEditMemberModalVisible} |
|
onCancel={() => setIsEditMemberModalVisible(false)} |
|
onSubmit={handleMemberUpdate} |
|
initialData={selectedEditMember} |
|
mode="edit" |
|
/> |
|
</TabPanel> |
|
</TabPanels> |
|
</TabGroup> |
|
|
|
<UserSearchModal |
|
isVisible={isAddMemberModalVisible} |
|
onCancel={() => setIsAddMemberModalVisible(false)} |
|
onSubmit={handleMemberCreate} |
|
accessToken={accessToken} |
|
/> |
|
</div> |
|
); |
|
}; |
|
|
|
export default TeamInfoView; |