litellm-mirror/ui/litellm-dashboard/src/components/team/team_info.tsx
Ishaan Jaff 431b230f07
[UI] Bug Fix, team model selector (#10171)
* fix tooltip

* bug fix fix team model selector
2025-04-19 16:31:38 -07:00

496 lines
No EOL
16 KiB
TypeScript

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;