diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 9fbff17aa5..87ba0540e5 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -4411,4 +4411,79 @@ export const updateDefaultTeamSettings = async (accessToken: string, settings: R console.error("Failed to update default team settings:", error); throw error; } +}; + + + +export const getTeamPermissionsCall = async ( + accessToken: string, + teamId: string +) => { + try { + let url = proxyBaseUrl + ? `${proxyBaseUrl}/team/permissions_list?team_id=${teamId}` + : `/team/permissions_list?team_id=${teamId}`; + + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + }); + + 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("Team permissions response:", data); + return data; + } catch (error) { + console.error("Failed to get team permissions:", error); + throw error; + } +}; + + + +export const teamPermissionsUpdateCall = async ( + accessToken: string, + teamId: string, + permissions: string[] +) => { + try { + let url = proxyBaseUrl + ? `${proxyBaseUrl}/team/permissions_update` + : `/team/permissions_update`; + + + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + team_id: teamId, + team_member_permissions: permissions, + }), + }); + + 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("Team permissions response:", data); + return data; + } catch (error) { + console.error("Failed to update team permissions:", error); + throw error; + } }; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/team/member_permissions.tsx b/ui/litellm-dashboard/src/components/team/member_permissions.tsx new file mode 100644 index 0000000000..f3ee9f19c3 --- /dev/null +++ b/ui/litellm-dashboard/src/components/team/member_permissions.tsx @@ -0,0 +1,175 @@ +import React, { useState, useEffect } from "react"; +import { + Card, + Title, + Text, + Button as TremorButton, + Table, + TableHead, + TableHeaderCell, + TableBody, + TableRow, + TableCell, +} from "@tremor/react"; +import { Button, message, Checkbox, Empty } from "antd"; +import { ReloadOutlined, SaveOutlined } from "@ant-design/icons"; +import { getTeamPermissionsCall, teamPermissionsUpdateCall } from "@/components/networking"; +import { getPermissionInfo } from "./permission_definitions"; + +interface MemberPermissionsProps { + teamId: string; + accessToken: string | null; + canEditTeam: boolean; +} + +const MemberPermissions: React.FC = ({ + teamId, + accessToken, + canEditTeam, +}) => { + const [permissions, setPermissions] = useState([]); + const [selectedPermissions, setSelectedPermissions] = useState([]); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [hasChanges, setHasChanges] = useState(false); + + const fetchPermissions = async () => { + try { + setLoading(true); + if (!accessToken) return; + const response = await getTeamPermissionsCall(accessToken, teamId); + const allPermissions = response.all_available_permissions || []; + setPermissions(allPermissions); + const teamPermissions = response.team_member_permissions || []; + setSelectedPermissions(teamPermissions); + setHasChanges(false); + } catch (error) { + message.error("Failed to load permissions"); + console.error("Error fetching permissions:", error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchPermissions(); + }, [teamId, accessToken]); + + const handlePermissionChange = (permission: string, checked: boolean) => { + const newSelectedPermissions = checked + ? [...selectedPermissions, permission] + : selectedPermissions.filter((p) => p !== permission); + setSelectedPermissions(newSelectedPermissions); + setHasChanges(true); + }; + + const handleSave = async () => { + try { + if (!accessToken) return; + setSaving(true); + await teamPermissionsUpdateCall(accessToken, teamId, selectedPermissions); + message.success("Permissions updated successfully"); + setHasChanges(false); + } catch (error) { + message.error("Failed to update permissions"); + console.error("Error updating permissions:", error); + } finally { + setSaving(false); + } + }; + + const handleReset = () => { + fetchPermissions(); + }; + + if (loading) { + return
Loading permissions...
; + } + + const hasPermissions = permissions.length > 0; + + return ( + +
+ Member Permissions + {canEditTeam && hasChanges && ( +
+ + + Save Changes + +
+ )} +
+ + + Control what team members can do when they are not team admins. + + + {hasPermissions ? ( + + + + Method + Endpoint + Description + Access + + + + {permissions.map((permission) => { + const permInfo = getPermissionInfo(permission); + return ( + + + + {permInfo.method} + + + + + {permInfo.endpoint} + + + + {permInfo.description} + + + + handlePermissionChange(permission, e.target.checked) + } + disabled={!canEditTeam} + /> + + + ); + })} + +
+ ) : ( +
+ +
+ )} +
+ ); +}; + +export default MemberPermissions; diff --git a/ui/litellm-dashboard/src/components/team/permission_definitions.tsx b/ui/litellm-dashboard/src/components/team/permission_definitions.tsx new file mode 100644 index 0000000000..625f61997e --- /dev/null +++ b/ui/litellm-dashboard/src/components/team/permission_definitions.tsx @@ -0,0 +1,64 @@ +export interface PermissionInfo { + method: string; + endpoint: string; + description: string; + route: string; +} + +/** + * Map of permission endpoint patterns to their descriptions + */ +export const PERMISSION_DESCRIPTIONS: Record = { + '/key/generate': 'Member can generate a virtual key for this team', + '/key/update': 'Member can update a virtual key belonging to this team', + '/key/delete': 'Member can delete a virtual key belonging to this team', + '/key/info': 'Member can get info about a virtual key belonging to this team', + '/key/regenerate': 'Member can regenerate a virtual key belonging to this team', + '/key/{key_id}/regenerate': 'Member can regenerate a virtual key belonging to this team', + '/key/list': 'Member can list virtual keys belonging to this team', + '/key/block': 'Member can block a virtual key belonging to this team', + '/key/unblock': 'Member can unblock a virtual key belonging to this team' +}; + +/** + * Determines the HTTP method for a given permission endpoint + */ +export const getMethodForEndpoint = (endpoint: string): string => { + if (endpoint.includes('/info') || endpoint.includes('/list')) { + return 'GET'; + } + return 'POST'; +}; + +/** + * Parses a permission string into a structured PermissionInfo object + */ +export const getPermissionInfo = (permission: string): PermissionInfo => { + const method = getMethodForEndpoint(permission); + const endpoint = permission; + + // Find exact match or fallback to default description + let description = PERMISSION_DESCRIPTIONS[permission]; + + // If no exact match, try to find a partial match based on patterns + if (!description) { + for (const [pattern, desc] of Object.entries(PERMISSION_DESCRIPTIONS)) { + if (permission.includes(pattern)) { + description = desc; + break; + } + } + } + + // Fallback if no match found + if (!description) { + description = `Access ${permission}`; + } + + return { + method, + endpoint, + description, + route: permission + }; +}; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/team/team_info.tsx b/ui/litellm-dashboard/src/components/team/team_info.tsx index 20e9d23ccf..f44a49e9b6 100644 --- a/ui/litellm-dashboard/src/components/team/team_info.tsx +++ b/ui/litellm-dashboard/src/components/team/team_info.tsx @@ -20,6 +20,8 @@ import { 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'; @@ -30,10 +32,9 @@ 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 { Team } from "../key_team_helpers/key_list"; -interface TeamData { +export interface TeamData { team_id: string; team_info: { team_alias: string; @@ -62,15 +63,15 @@ interface TeamData { team_memberships: any[]; } -interface TeamInfoProps { +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; - onUpdate?: (team: Team) => void } const TeamInfoView: React.FC = ({ @@ -80,8 +81,7 @@ const TeamInfoView: React.FC = ({ is_team_admin, is_proxy_admin, userModels, - editTeam, - onUpdate + editTeam }) => { const [teamData, setTeamData] = useState(null); const [loading, setLoading] = useState(true); @@ -202,10 +202,7 @@ const TeamInfoView: React.FC = ({ }; const response = await teamUpdateCall(accessToken, updateData); - if (onUpdate) { - onUpdate(response.data) - } - + message.success("Team settings updated successfully"); setIsEditing(false); fetchTeamInfo(); @@ -241,6 +238,7 @@ const TeamInfoView: React.FC = ({ Overview, ...(canEditTeam ? [ Members, + Member Permissions, Settings ] : []) ]} @@ -287,58 +285,23 @@ const TeamInfoView: React.FC = ({ {/* Members Panel */} -
- - - - - User ID - User Email - Role - - - + + - - {teamData.team_info.members_with_roles.map((member: Member, index: number) => ( - - - {member.user_id} - - - {member.user_email} - - - {member.role} - - - {canEditTeam && ( - <> - { - setSelectedEditMember(member); - setIsEditMemberModalVisible(true); - }} - /> - handleMemberDelete(member)} - icon={TrashIcon} - size="sm" - /> - - )} - - - ))} - -
-
- setIsAddMemberModalVisible(true)}> - Add Member - -
+ {/* Member Permissions Panel */} + + {/* Settings Panel */} diff --git a/ui/litellm-dashboard/src/components/team/team_member_view.tsx b/ui/litellm-dashboard/src/components/team/team_member_view.tsx new file mode 100644 index 0000000000..cd6818cb28 --- /dev/null +++ b/ui/litellm-dashboard/src/components/team/team_member_view.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { Member } from "@/components/networking"; +import { + Card, + Table, + TableHead, + TableRow, + TableHeaderCell, + TableBody, + TableCell, + Text, + Icon, + Button as TremorButton, +} from '@tremor/react'; +import { + TeamData, +} from './team_info'; +import { PencilAltIcon, PlusIcon, TrashIcon } from "@heroicons/react/outline"; + +interface TeamMembersComponentProps { + teamData: TeamData; + canEditTeam: boolean; + handleMemberDelete: (member: Member) => void; + setSelectedEditMember: (member: Member) => void; + setIsEditMemberModalVisible: (visible: boolean) => void; + setIsAddMemberModalVisible: (visible: boolean) => void; +} + +const TeamMembersComponent: React.FC = ({ + teamData, + canEditTeam, + handleMemberDelete, + setSelectedEditMember, + setIsEditMemberModalVisible, + setIsAddMemberModalVisible, +}) => { + return ( +
+ + + + + User ID + User Email + Role + + + + + + {teamData.team_info.members_with_roles.map((member: Member, index: number) => ( + + + {member.user_id} + + + {member.user_email} + + + {member.role} + + + {canEditTeam && ( + <> + { + setSelectedEditMember(member); + setIsEditMemberModalVisible(true); + }} + /> + handleMemberDelete(member)} + /> + + )} + + + ))} + +
+
+ setIsAddMemberModalVisible(true)}> + Add Member + +
+ ); +}; + +export default TeamMembersComponent; diff --git a/ui/litellm-dashboard/src/components/teams.tsx b/ui/litellm-dashboard/src/components/teams.tsx index 1ce94573f3..7e2b48895a 100644 --- a/ui/litellm-dashboard/src/components/teams.tsx +++ b/ui/litellm-dashboard/src/components/teams.tsx @@ -324,7 +324,7 @@ const Teams: React.FC = ({ {selectedTeamId ? ( { + onUpdate={(data) => { setTeams(teams => { if (teams == null) { return teams;