Spaces:
Paused
Paused
| import React, { useState, useEffect } from "react"; | |
| import NumericalInput from "../shared/numerical_input"; | |
| 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 TeamMembersComponent from "./team_member_view"; | |
| import MemberPermissions from "./member_permissions"; | |
| import { teamInfoCall, teamMemberDeleteCall, teamMemberAddCall, teamMemberUpdateCall, Member, teamUpdateCall } from "@/components/networking"; | |
| import { Button, Form, Input, Select, message, Tooltip } from "antd"; | |
| import { InfoCircleOutlined } from '@ant-design/icons'; | |
| import { | |
| Select as Select2, | |
| } from "antd"; | |
| import { PencilAltIcon, PlusIcon, TrashIcon } from "@heroicons/react/outline"; | |
| import MemberModal from "./edit_membership"; | |
| import UserSearchModal from "@/components/common_components/user_search_modal"; | |
| import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key"; | |
| import { isAdminRole } from "@/utils/roles"; | |
| export 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: { | |
| model_aliases: Record<string, string>; | |
| } | null; | |
| created_at: string; | |
| }; | |
| keys: any[]; | |
| team_memberships: any[]; | |
| } | |
| export interface TeamInfoProps { | |
| teamId: string; | |
| onUpdate: (data: any) => void; | |
| onClose: () => void; | |
| accessToken: string | null; | |
| is_team_admin: boolean; | |
| is_proxy_admin: boolean; | |
| userModels: string[]; | |
| editTeam: boolean; | |
| } | |
| const TeamInfoView: React.FC<TeamInfoProps> = ({ | |
| teamId, | |
| onClose, | |
| accessToken, | |
| is_team_admin, | |
| is_proxy_admin, | |
| userModels, | |
| editTeam | |
| }) => { | |
| 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 [isEditing, setIsEditing] = useState(false); | |
| console.log("userModels in team info", userModels); | |
| const canEditTeam = 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 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); | |
| } | |
| }; | |
| 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 handleTeamUpdate = async (values: any) => { | |
| try { | |
| if (!accessToken) return; | |
| let parsedMetadata = {}; | |
| try { | |
| parsedMetadata = values.metadata ? JSON.parse(values.metadata) : {}; | |
| } catch (e) { | |
| message.error("Invalid JSON in metadata field"); | |
| return; | |
| } | |
| const updateData = { | |
| team_id: teamId, | |
| team_alias: values.team_alias, | |
| models: values.models, | |
| tpm_limit: values.tpm_limit, | |
| rpm_limit: values.rpm_limit, | |
| max_budget: values.max_budget, | |
| budget_duration: values.budget_duration, | |
| metadata: { | |
| ...parsedMetadata, | |
| guardrails: values.guardrails || [] | |
| } | |
| }; | |
| const response = await teamUpdateCall(accessToken, updateData); | |
| message.success("Team settings updated successfully"); | |
| setIsEditing(false); | |
| fetchTeamInfo(); | |
| } catch (error) { | |
| message.error("Failed to update team settings"); | |
| console.error("Error updating team:", 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; | |
| 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 defaultIndex={editTeam ? 3 : 0}> | |
| <TabList className="mb-4"> | |
| {[ | |
| <Tab key="overview">Overview</Tab>, | |
| ...(canEditTeam ? [ | |
| <Tab key="members">Members</Tab>, | |
| <Tab key="member-permissions">Member Permissions</Tab>, | |
| <Tab key="settings">Settings</Tab> | |
| ] : []) | |
| ]} | |
| </TabList> | |
| <TabPanels> | |
| {/* Overview Panel */} | |
| <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> | |
| {/* Members Panel */} | |
| <TabPanel> | |
| <TeamMembersComponent | |
| teamData={teamData} | |
| canEditTeam={canEditTeam} | |
| handleMemberDelete={handleMemberDelete} | |
| setSelectedEditMember={setSelectedEditMember} | |
| setIsEditMemberModalVisible={setIsEditMemberModalVisible} | |
| setIsAddMemberModalVisible={setIsAddMemberModalVisible} | |
| /> | |
| </TabPanel> | |
| {/* Member Permissions Panel */} | |
| {canEditTeam && ( | |
| <TabPanel> | |
| <MemberPermissions | |
| teamId={teamId} | |
| accessToken={accessToken} | |
| canEditTeam={canEditTeam} | |
| /> | |
| </TabPanel> | |
| )} | |
| {/* Settings Panel */} | |
| <TabPanel> | |
| <Card> | |
| <div className="flex justify-between items-center mb-4"> | |
| <Title>Team Settings</Title> | |
| {(canEditTeam && !isEditing) && ( | |
| <TremorButton | |
| onClick={() => setIsEditing(true)} | |
| > | |
| Edit Settings | |
| </TremorButton> | |
| )} | |
| </div> | |
| {isEditing ? ( | |
| <Form | |
| form={form} | |
| onFinish={handleTeamUpdate} | |
| initialValues={{ | |
| ...info, | |
| team_alias: info.team_alias, | |
| models: info.models, | |
| tpm_limit: info.tpm_limit, | |
| rpm_limit: info.rpm_limit, | |
| max_budget: info.max_budget, | |
| budget_duration: info.budget_duration, | |
| guardrails: info.metadata?.guardrails || [], | |
| metadata: info.metadata ? JSON.stringify(info.metadata, null, 2) : "", | |
| }} | |
| layout="vertical" | |
| > | |
| <Form.Item | |
| label="Team Name" | |
| name="team_alias" | |
| rules={[{ required: true, message: "Please input a team name" }]} | |
| > | |
| <Input type=""/> | |
| </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, idx) => ( | |
| <Select.Option key={idx} 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={ | |
| <span> | |
| Guardrails{' '} | |
| <Tooltip title="Setup your first guardrail"> | |
| <a | |
| href="https://docs.litellm.ai/docs/proxy/guardrails/quick_start" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| onClick={(e) => e.stopPropagation()} | |
| > | |
| <InfoCircleOutlined style={{ marginLeft: '4px' }} /> | |
| </a> | |
| </Tooltip> | |
| </span> | |
| } | |
| name="guardrails" | |
| help="Select existing guardrails or enter new ones" | |
| > | |
| <Select | |
| mode="tags" | |
| placeholder="Select or enter guardrails" | |
| /> | |
| </Form.Item> | |
| <Form.Item label="Metadata" name="metadata"> | |
| <Input.TextArea rows={10} /> | |
| </Form.Item> | |
| <div className="flex justify-end gap-2 mt-6"> | |
| <Button onClick={() => setIsEditing(false)}> | |
| Cancel | |
| </Button> | |
| <TremorButton> | |
| Save Changes | |
| </TremorButton> | |
| </div> | |
| </Form> | |
| ) : ( | |
| <div className="space-y-4"> | |
| <div> | |
| <Text className="font-medium">Team Name</Text> | |
| <div>{info.team_alias}</div> | |
| </div> | |
| <div> | |
| <Text className="font-medium">Team ID</Text> | |
| <div className="font-mono">{info.team_id}</div> | |
| </div> | |
| <div> | |
| <Text className="font-medium">Created At</Text> | |
| <div>{new Date(info.created_at).toLocaleString()}</div> | |
| </div> | |
| <div> | |
| <Text className="font-medium">Models</Text> | |
| <div className="flex flex-wrap gap-2 mt-1"> | |
| {info.models.map((model, index) => ( | |
| <Badge key={index} color="red"> | |
| {model} | |
| </Badge> | |
| ))} | |
| </div> | |
| </div> | |
| <div> | |
| <Text className="font-medium">Rate Limits</Text> | |
| <div>TPM: {info.tpm_limit || 'Unlimited'}</div> | |
| <div>RPM: {info.rpm_limit || 'Unlimited'}</div> | |
| </div> | |
| <div> | |
| <Text className="font-medium">Budget</Text> | |
| <div>Max: {info.max_budget !== null ? `$${info.max_budget}` : 'No Limit'}</div> | |
| <div>Reset: {info.budget_duration || 'Never'}</div> | |
| </div> | |
| <div> | |
| <Text className="font-medium">Status</Text> | |
| <Badge color={info.blocked ? 'red' : 'green'}> | |
| {info.blocked ? 'Blocked' : 'Active'} | |
| </Badge> | |
| </div> | |
| </div> | |
| )} | |
| </Card> | |
| </TabPanel> | |
| </TabPanels> | |
| </TabGroup> | |
| <MemberModal | |
| visible={isEditMemberModalVisible} | |
| onCancel={() => setIsEditMemberModalVisible(false)} | |
| onSubmit={handleMemberUpdate} | |
| initialData={selectedEditMember} | |
| mode="edit" | |
| config={{ | |
| title: "Edit Member", | |
| showEmail: true, | |
| showUserId: true, | |
| roleOptions: [ | |
| { label: "Admin", value: "admin" }, | |
| { label: "User", value: "user" } | |
| ] | |
| }} | |
| /> | |
| <UserSearchModal | |
| isVisible={isAddMemberModalVisible} | |
| onCancel={() => setIsAddMemberModalVisible(false)} | |
| onSubmit={handleMemberCreate} | |
| accessToken={accessToken} | |
| /> | |
| </div> | |
| ); | |
| }; | |
| export default TeamInfoView; |