diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts
index 435000f9c88..0ce0cab183f 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts
+++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/useAuthorized.ts
@@ -5,6 +5,35 @@ import { useRouter } from "next/navigation";
import { jwtDecode } from "jwt-decode";
import { clearTokenCookies, getCookie } from "@/utils/cookieUtils";
+function formatUserRole(userRole: string) {
+ if (!userRole) {
+ return "Undefined Role";
+ }
+ switch (userRole.toLowerCase()) {
+ case "app_owner":
+ return "App Owner";
+ case "demo_app_owner":
+ return "App Owner";
+ case "app_admin":
+ return "Admin";
+ case "proxy_admin":
+ return "Admin";
+ case "proxy_admin_viewer":
+ return "Admin Viewer";
+ case "org_admin":
+ return "Org Admin";
+ case "internal_user":
+ return "Internal User";
+ case "internal_user_viewer":
+ case "internal_viewer": // TODO:remove if deprecated
+ return "Internal Viewer";
+ case "app_user":
+ return "App User";
+ default:
+ return "Unknown Role";
+ }
+}
+
const useAuthorized = () => {
const router = useRouter();
@@ -35,7 +64,7 @@ const useAuthorized = () => {
accessToken: decoded?.key ?? null,
userId: decoded?.user_id ?? null,
userEmail: decoded?.user_email ?? null,
- userRole: decoded?.user_role ?? null,
+ userRole: formatUserRole(decoded?.user_role ?? null),
premiumUser: decoded?.premium_user ?? null,
disabledPersonalKeyCreation: decoded?.disabled_non_admin_personal_key_creation ?? null,
showSSOBanner: decoded?.login_method === "username_password" ?? false,
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx
index 68ab361356f..97837ff8e0a 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx
@@ -25,7 +25,7 @@ function withBase(path: string): string {
export default function Layout({ children }: { children: React.ReactNode }) {
const router = useRouter();
const searchParams = useSearchParams();
- const { accessToken, userRole } = useAuthorized();
+ const { accessToken, userRole, userId, userEmail, premiumUser } = useAuthorized();
const [sidebarCollapsed, setSidebarCollapsed] = React.useState(false);
const [page, setPage] = useState(() => {
return searchParams.get("page") || "api-keys";
@@ -51,15 +51,13 @@ export default function Layout({ children }: { children: React.ReactNode }) {
isPublicPage={false}
sidebarCollapsed={sidebarCollapsed}
onToggleSidebar={toggleSidebar}
- userID={null}
- userEmail={null}
- userRole={null}
- premiumUser={false}
+ userID={userId}
+ userEmail={userEmail}
+ userRole={userRole}
+ premiumUser={premiumUser}
proxySettings={undefined}
- setProxySettings={function (value: any): void {
- throw new Error("Function not implemented.");
- }}
- accessToken={null}
+ setProxySettings={() => {}}
+ accessToken={accessToken}
/>
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/TeamsView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/TeamsView.tsx
new file mode 100644
index 00000000000..fa0ec060946
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/TeamsView.tsx
@@ -0,0 +1,359 @@
+import React, { useState, useEffect } from "react";
+import { teamDeleteCall, Organization } from "@/components/networking";
+import { fetchTeams } from "@/components/common_components/fetch_teams";
+import { Form } from "antd";
+import TeamInfoView from "@/components/team/team_info";
+import TeamSSOSettings from "@/components/TeamSSOSettings";
+import { isAdminRole } from "@/utils/roles";
+import { Card, Button, Col, Text, Grid, TabPanel } from "@tremor/react";
+import AvailableTeamsPanel from "@/components/team/available_teams";
+import type { KeyResponse, Team } from "@/components/key_team_helpers/key_list";
+
+import { Member, v2TeamListCall } from "@/components/networking";
+import { updateExistingKeys } from "@/utils/dataUtils";
+import TeamsHeaderTabs from "@/app/(dashboard)/teams/components/TeamsHeaderTabs";
+import TeamsFilters from "@/app/(dashboard)/teams/components/TeamsFilters";
+import useFetchTeams from "@/app/(dashboard)/teams/hooks/useFetchTeams";
+import TeamsTable from "@/app/(dashboard)/teams/components/TeamsTable/TeamsTable";
+import DeleteTeamModal from "@/app/(dashboard)/teams/components/modals/DeleteTeamModal";
+import CreateTeamModal from "@/app/(dashboard)/teams/components/modals/CreateTeamModal";
+
+interface TeamProps {
+ teams: Team[] | null;
+ accessToken: string | null;
+ setTeams: React.Dispatch
>;
+ userID: string | null;
+ userRole: string | null;
+ organizations: Organization[] | null;
+ premiumUser?: boolean;
+}
+
+interface FilterState {
+ team_id: string;
+ team_alias: string;
+ organization_id: string;
+ sort_by: string;
+ sort_order: "asc" | "desc";
+}
+
+interface TeamInfo {
+ members_with_roles: Member[];
+}
+
+interface PerTeamInfo {
+ keys: KeyResponse[];
+ team_info: TeamInfo;
+}
+
+const TeamsView: React.FC = ({
+ teams,
+ accessToken,
+ setTeams,
+ userID,
+ userRole,
+ organizations,
+ premiumUser = false,
+}) => {
+ const [currentOrg, setCurrentOrg] = useState(null);
+ const [showFilters, setShowFilters] = useState(false);
+ const [filters, setFilters] = useState({
+ team_id: "",
+ team_alias: "",
+ organization_id: "",
+ sort_by: "created_at",
+ sort_order: "desc",
+ });
+
+ const [form] = Form.useForm();
+ const [memberForm] = Form.useForm();
+
+ const [selectedTeamId, setSelectedTeamId] = useState(null);
+ const [editTeam, setEditTeam] = useState(false);
+
+ const [isTeamModalVisible, setIsTeamModalVisible] = useState(false);
+ const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false);
+ const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false);
+ const [userModels, setUserModels] = useState([]);
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+ const [teamToDelete, setTeamToDelete] = useState(null);
+ const [perTeamInfo, setPerTeamInfo] = useState>({});
+
+ const [loggingSettings, setLoggingSettings] = useState([]);
+ const [modelAliases, setModelAliases] = useState<{ [key: string]: string }>({});
+ const { lastRefreshed, onRefreshClick: handleRefreshClick } = useFetchTeams({ currentOrg, setTeams });
+
+ useEffect(() => {
+ const fetchTeamInfo = () => {
+ if (!teams) return;
+
+ const newPerTeamInfo = teams.reduce(
+ (acc, team) => {
+ acc[team.team_id] = {
+ keys: team.keys || [],
+ team_info: {
+ members_with_roles: team.members_with_roles || [],
+ },
+ };
+ return acc;
+ },
+ {} as Record,
+ );
+
+ setPerTeamInfo(newPerTeamInfo);
+ };
+
+ fetchTeamInfo();
+ }, [teams]);
+
+ const handleOk = () => {
+ setIsTeamModalVisible(false);
+ form.resetFields();
+ setLoggingSettings([]);
+ setModelAliases({});
+ };
+
+ const handleMemberOk = () => {
+ setIsAddMemberModalVisible(false);
+ setIsEditMemberModalVisible(false);
+ memberForm.resetFields();
+ };
+
+ const handleCancel = () => {
+ setIsTeamModalVisible(false);
+ form.resetFields();
+ setLoggingSettings([]);
+ setModelAliases({});
+ };
+
+ const handleDelete = async (team_id: string) => {
+ // Set the team to delete and open the confirmation modal
+ setTeamToDelete(team_id);
+ setIsDeleteModalOpen(true);
+ };
+
+ const confirmDelete = async () => {
+ if (teamToDelete == null || teams == null || accessToken == null) {
+ return;
+ }
+
+ try {
+ await teamDeleteCall(accessToken, teamToDelete);
+ // Successfully completed the deletion. Update the state to trigger a rerender.
+ fetchTeams(accessToken, userID, userRole, currentOrg, setTeams);
+ } catch (error) {
+ console.error("Error deleting the team:", error);
+ // Handle any error situations, such as displaying an error message to the user.
+ }
+
+ // Close the confirmation modal and reset the teamToDelete
+ setIsDeleteModalOpen(false);
+ setTeamToDelete(null);
+ };
+
+ const cancelDelete = () => {
+ // Close the confirmation modal and reset the teamToDelete
+ setIsDeleteModalOpen(false);
+ setTeamToDelete(null);
+ };
+
+ const is_team_admin = (team: any) => {
+ if (team == null || team.members_with_roles == null) {
+ return false;
+ }
+ for (let i = 0; i < team.members_with_roles.length; i++) {
+ let member = team.members_with_roles[i];
+ if (member.user_id == userID && member.role == "admin") {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ const handleFilterChange = (key: keyof FilterState, value: string) => {
+ const newFilters = { ...filters, [key]: value };
+ setFilters(newFilters);
+ // Call teamListCall with the new filters
+ if (accessToken) {
+ v2TeamListCall(
+ accessToken,
+ newFilters.organization_id || null,
+ null,
+ newFilters.team_id || null,
+ newFilters.team_alias || null,
+ )
+ .then((response) => {
+ if (response && response.teams) {
+ setTeams(response.teams);
+ }
+ })
+ .catch((error) => {
+ console.error("Error fetching teams:", error);
+ });
+ }
+ };
+
+ const handleSortChange = (sortBy: string, sortOrder: "asc" | "desc") => {
+ const newFilters = {
+ ...filters,
+ sort_by: sortBy,
+ sort_order: sortOrder,
+ };
+ setFilters(newFilters);
+ // Call teamListCall with the new sort parameters
+ if (accessToken) {
+ v2TeamListCall(
+ accessToken,
+ filters.organization_id || null,
+ null,
+ filters.team_id || null,
+ filters.team_alias || null,
+ )
+ .then((response) => {
+ if (response && response.teams) {
+ setTeams(response.teams);
+ }
+ })
+ .catch((error) => {
+ console.error("Error fetching teams:", error);
+ });
+ }
+ };
+
+ const handleFilterReset = () => {
+ setFilters({
+ team_id: "",
+ team_alias: "",
+ organization_id: "",
+ sort_by: "created_at",
+ sort_order: "desc",
+ });
+ // Reset teams list
+ if (accessToken) {
+ v2TeamListCall(accessToken, null, userID || null, null, null)
+ .then((response) => {
+ if (response && response.teams) {
+ setTeams(response.teams);
+ }
+ })
+ .catch((error) => {
+ console.error("Error fetching teams:", error);
+ });
+ }
+ };
+
+ return (
+
+
+
+ {(userRole == "Admin" || userRole == "Org Admin") && (
+
+ )}
+ {selectedTeamId ? (
+ {
+ setTeams((teams) => {
+ if (teams == null) {
+ return teams;
+ }
+ const updated = teams.map((team) => {
+ if (data.team_id === team.team_id) {
+ return updateExistingKeys(team, data);
+ }
+ return team;
+ });
+ // Minimal fix: refresh the full team list after an update
+ if (accessToken) {
+ fetchTeams(accessToken, userID, userRole, currentOrg, setTeams);
+ }
+ return updated;
+ });
+ }}
+ onClose={() => {
+ setSelectedTeamId(null);
+ setEditTeam(false);
+ }}
+ accessToken={accessToken}
+ is_team_admin={is_team_admin(teams?.find((team) => team.team_id === selectedTeamId))}
+ is_proxy_admin={userRole == "Admin"}
+ userModels={userModels}
+ editTeam={editTeam}
+ />
+ ) : (
+
+
+
+ Click on “Team ID” to view team details and manage team members.
+
+
+
+
+
+
+ {isDeleteModalOpen && (
+
+ )}
+
+
+
+
+
+
+
+ {isAdminRole(userRole || "") && (
+
+
+
+ )}
+
+ )}
+ {(userRole == "Admin" || userRole == "Org Admin") && (
+
+ )}
+
+
+
+ );
+};
+
+export default TeamsView;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsFilters.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsFilters.tsx
new file mode 100644
index 00000000000..3c7d0951a5e
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsFilters.tsx
@@ -0,0 +1,141 @@
+import { Select, SelectItem } from "@tremor/react";
+import React from "react";
+import { Organization } from "@/components/networking";
+
+interface TeamsFiltersProps {
+ filters: FilterState;
+ organizations: Organization[] | null;
+ showFilters: boolean;
+ onToggleFilters: (toggle: boolean) => void;
+ onChange: (key: K, value: FilterState[K]) => void;
+ onReset: () => void;
+}
+
+type FilterState = {
+ team_id: string;
+ team_alias: string;
+ organization_id: string;
+ sort_by: string;
+ sort_order: "asc" | "desc";
+};
+
+const TeamsFilters = ({
+ filters,
+ organizations,
+ showFilters,
+ onToggleFilters,
+ onChange,
+ onReset,
+}: TeamsFiltersProps) => {
+ return (
+
+ {/* Search and Filter Controls */}
+
+ {/* Team Alias Search */}
+
+
onChange("team_alias", e.target.value)}
+ />
+
+
+
+ {/* Filter Button */}
+
+
+ {/* Reset Filters Button */}
+
+
+
+ {/* Additional Filters */}
+ {showFilters && (
+
+ {/* Team ID Search */}
+
+
onChange("team_id", e.target.value)}
+ />
+
+
+
+ {/* Organization Dropdown */}
+
+
+
+
+ )}
+
+ );
+};
+
+export default TeamsFilters;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsHeaderTabs.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsHeaderTabs.tsx
new file mode 100644
index 00000000000..d57cb3a74f0
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsHeaderTabs.tsx
@@ -0,0 +1,38 @@
+import { Icon, Tab, TabGroup, TabList, TabPanels, Text } from "@tremor/react";
+import { isAdminRole } from "@/utils/roles";
+import { RefreshIcon } from "@heroicons/react/outline";
+import React from "react";
+
+type TeamsHeaderTabsProps = {
+ lastRefreshed: string;
+ onRefresh: () => void;
+ userRole: string | null;
+ children: React.ReactNode;
+};
+
+const TeamsHeaderTabs = ({ lastRefreshed, onRefresh, userRole, children }: TeamsHeaderTabsProps) => {
+ return (
+
+
+
+ Your Teams
+ Available Teams
+ {isAdminRole(userRole || "") && Default Team Settings}
+
+
+ {lastRefreshed && Last Refreshed: {lastRefreshed}}
+
+
+
+ {children}
+
+ );
+};
+
+export default TeamsHeaderTabs;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/ModelsCell.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/ModelsCell.tsx
new file mode 100644
index 00000000000..5cabe4c4a8f
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/ModelsCell.tsx
@@ -0,0 +1,98 @@
+import { Badge, Icon, TableCell, Text } from "@tremor/react";
+import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline";
+import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key";
+import React, { useState } from "react";
+import { Team } from "@/components/key_team_helpers/key_list";
+
+interface ModelsCellProps {
+ team: Team;
+}
+
+const ModelsCell = ({ team }: ModelsCellProps) => {
+ const [expandedAccordion, setExpandedAccordion] = useState(false);
+
+ return (
+ 3 ? "px-0" : ""}
+ >
+
+ {Array.isArray(team.models) ? (
+
+ {team.models.length === 0 ? (
+
+ All Proxy Models
+
+ ) : (
+ <>
+
+ {team.models.length > 3 && (
+
+ {
+ setExpandedAccordion((prev) => !prev);
+ }}
+ />
+
+ )}
+
+ {team.models.slice(0, 3).map((model: string, index: number) =>
+ model === "all-proxy-models" ? (
+
+ All Proxy Models
+
+ ) : (
+
+
+ {model.length > 30
+ ? `${getModelDisplayName(model).slice(0, 30)}...`
+ : getModelDisplayName(model)}
+
+
+ ),
+ )}
+ {team.models.length > 3 && !expandedAccordion && (
+
+
+ +{team.models.length - 3} {team.models.length - 3 === 1 ? "more model" : "more models"}
+
+
+ )}
+ {expandedAccordion && (
+
+ {team.models.slice(3).map((model: string, index: number) =>
+ model === "all-proxy-models" ? (
+
+ All Proxy Models
+
+ ) : (
+
+
+ {model.length > 30
+ ? `${getModelDisplayName(model).slice(0, 30)}...`
+ : getModelDisplayName(model)}
+
+
+ ),
+ )}
+
+ )}
+
+
+ >
+ )}
+
+ ) : null}
+
+
+ );
+};
+
+export default ModelsCell;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/TeamsTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/TeamsTable.tsx
new file mode 100644
index 00000000000..4e8b3286c81
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/TeamsTable.tsx
@@ -0,0 +1,177 @@
+import {
+ Badge,
+ Button,
+ Icon,
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeaderCell,
+ TableRow,
+ Text,
+} from "@tremor/react";
+import { Tooltip } from "antd";
+import { formatNumberWithCommas } from "@/utils/dataUtils";
+import { ChevronDownIcon, ChevronRightIcon, PencilAltIcon, TrashIcon } from "@heroicons/react/outline";
+import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key";
+import React, { useState } from "react";
+import { type KeyResponse, Team } from "@/components/key_team_helpers/key_list";
+import { Member, Organization } from "@/components/networking";
+import ModelsCell from "@/app/(dashboard)/teams/components/TeamsTable/ModelsCell";
+import YourRoleCell from "@/app/(dashboard)/teams/components/TeamsTable/YourRoleCell/YourRoleCell";
+
+type TeamsTableProps = {
+ teams: Team[] | null;
+ currentOrg: Organization | null;
+ perTeamInfo: Record;
+ userRole: string | null;
+ userId: string | null;
+ setSelectedTeamId: (teamId: string) => void;
+ setEditTeam: (editTeam: boolean) => void;
+ onDeleteTeam: (teamId: string) => void;
+};
+
+interface TeamInfo {
+ members_with_roles: Member[];
+}
+
+interface PerTeamInfo {
+ keys: KeyResponse[];
+ team_info: TeamInfo;
+}
+
+const TeamsTable = ({
+ teams,
+ currentOrg,
+ setSelectedTeamId,
+ perTeamInfo,
+ userRole,
+ userId,
+ setEditTeam,
+ onDeleteTeam,
+}: TeamsTableProps) => {
+ return (
+
+
+
+ Team Name
+ Team ID
+ Created
+ Spend (USD)
+ Budget (USD)
+ Models
+ Organization
+ Your Role
+ Info
+
+
+
+
+ {teams && teams.length > 0
+ ? teams
+ .filter((team) => {
+ if (!currentOrg) return true;
+ return team.organization_id === currentOrg.organization_id;
+ })
+ .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
+ .map((team: any) => (
+
+
+ {team["team_alias"]}
+
+
+
+
+
+
+
+
+
+ {team.created_at ? new Date(team.created_at).toLocaleDateString() : "N/A"}
+
+
+ {formatNumberWithCommas(team["spend"], 4)}
+
+
+ {team["max_budget"] !== null && team["max_budget"] !== undefined ? team["max_budget"] : "No limit"}
+
+
+ {team.organization_id}
+
+
+
+ {perTeamInfo &&
+ team.team_id &&
+ perTeamInfo[team.team_id] &&
+ perTeamInfo[team.team_id].keys &&
+ perTeamInfo[team.team_id].keys.length}{" "}
+ Keys
+
+
+ {perTeamInfo &&
+ team.team_id &&
+ perTeamInfo[team.team_id] &&
+ perTeamInfo[team.team_id].team_info &&
+ perTeamInfo[team.team_id].team_info.members_with_roles &&
+ perTeamInfo[team.team_id].team_info.members_with_roles.length}{" "}
+ Members
+
+
+
+ {userRole == "Admin" ? (
+ <>
+ {
+ setSelectedTeamId(team.team_id);
+ setEditTeam(true);
+ }}
+ />
+ onDeleteTeam(team.team_id)} icon={TrashIcon} size="sm" />
+ >
+ ) : null}
+
+
+ ))
+ : null}
+
+
+ );
+};
+
+export default TeamsTable;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/YourRoleCell/TeamRoleBadge.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/YourRoleCell/TeamRoleBadge.test.tsx
new file mode 100644
index 00000000000..b7e659403cb
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/YourRoleCell/TeamRoleBadge.test.tsx
@@ -0,0 +1,38 @@
+import React from "react";
+import { describe, it, expect } from "vitest";
+import { render, screen } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import TeamRoleBadge from "./TeamRoleBadge";
+
+const renderBadge = (role: string | null) => render({TeamRoleBadge(role)}
);
+
+describe("TeamRoleBadge", () => {
+ it("renders admin badge with correct label, base classes, styles, and an icon", () => {
+ renderBadge("admin");
+ const label = screen.getByText("Admin");
+ const badge = label.closest("span")!;
+ expect(badge).toHaveClass("inline-flex", "items-center", "border", "text-xs", "font-medium");
+ expect(badge).toHaveStyle({
+ backgroundColor: "#EEF2FF",
+ color: "#3730A3",
+ borderColor: "#C7D2FE",
+ });
+ expect(badge.querySelector("svg")).toBeInTheDocument(); // ShieldIcon renders as an SVG
+ });
+
+ it.each<[string | null]>([["user"], [null], ["viewer" as unknown as string]])(
+ "renders member badge for non-admin role (%p) with correct styles",
+ (role) => {
+ renderBadge(role);
+ const label = screen.getByText("Member");
+ const badge = label.closest("span")!;
+ expect(badge).toHaveClass("inline-flex", "items-center", "border", "text-xs", "font-medium");
+ expect(badge).toHaveStyle({
+ backgroundColor: "#F3F4F6",
+ color: "#4B5563",
+ borderColor: "#E5E7EB",
+ });
+ expect(badge.querySelector("svg")).toBeInTheDocument(); // UserIcon renders as an SVG
+ },
+ );
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/YourRoleCell/TeamRoleBadge.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/YourRoleCell/TeamRoleBadge.tsx
new file mode 100644
index 00000000000..394b5d85348
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/YourRoleCell/TeamRoleBadge.tsx
@@ -0,0 +1,47 @@
+import { ShieldIcon, UserIcon } from "lucide-react";
+
+const MEMBER_BADGE_BG = "#F3F4F6"; // gray-100
+const MEMBER_BADGE_TEXT = "#4B5563"; // gray-600
+const MEMBER_BADGE_BORDER = "#E5E7EB"; // gray-200
+
+const ADMIN_BADGE_BG = "#EEF2FF"; // indigo-50
+const ADMIN_BADGE_TEXT = "#3730A3"; // indigo-800
+const ADMIN_BADGE_BORDER = "#C7D2FE"; // indigo-200
+
+const TeamRoleBadge = (role: string | null) => {
+ const base = "inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-medium border";
+
+ switch (role) {
+ case "admin":
+ return (
+
+
+ Admin
+
+ );
+ case "user":
+ default:
+ return (
+
+
+ Member
+
+ );
+ }
+};
+
+export default TeamRoleBadge;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/YourRoleCell/YourRoleCell.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/YourRoleCell/YourRoleCell.test.tsx
new file mode 100644
index 00000000000..20a4497159d
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/YourRoleCell/YourRoleCell.test.tsx
@@ -0,0 +1,43 @@
+import React from "react";
+import { describe, it, expect, vi } from "vitest";
+import { render, screen } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import type { Team } from "@/components/key_team_helpers/key_list";
+import YourRoleCell from "./YourRoleCell";
+
+// Lightweight mocks for stable, focused tests
+vi.mock("@tremor/react", () => ({
+ TableCell: ({ children }: { children: React.ReactNode }) => {children}
,
+}));
+
+// The component invokes TeamRoleBadge as a function, so mock it as such
+vi.mock("@/app/(dashboard)/teams/components/TeamsTable/YourRoleCell/TeamRoleBadge", () => ({
+ __esModule: true,
+ default: (role: string | null) => {role === "admin" ? "Admin" : "Member"},
+}));
+
+const team = (members?: Array<{ user_id: string; role: "admin" | "user" }>): Team =>
+ ({ members_with_roles: members }) as unknown as Team;
+
+describe("YourRoleCell", () => {
+ it("renders Admin when the user is an admin of the team", () => {
+ render();
+ expect(screen.getByTestId("cell")).toBeInTheDocument();
+ expect(screen.getByTestId("badge")).toHaveTextContent("Admin");
+ });
+
+ it("renders Member when the user is a regular member", () => {
+ render();
+ expect(screen.getByTestId("badge")).toHaveTextContent("Member");
+ });
+
+ it.each<[string, Team, string | null]>([
+ ["userId is null", team([{ user_id: "u3", role: "admin" }]), null],
+ ["user not in team", team([{ user_id: "x", role: "user" }]), "y"],
+ ["team has no members", team([]), "u4"],
+ ["members field undefined", team(undefined), "u5"],
+ ])("falls back to Member when no role can be determined (%s)", (_label, t, uid) => {
+ render();
+ expect(screen.getByTestId("badge")).toHaveTextContent("Member");
+ });
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/YourRoleCell/YourRoleCell.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/YourRoleCell/YourRoleCell.tsx
new file mode 100644
index 00000000000..66592943fd5
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/YourRoleCell/YourRoleCell.tsx
@@ -0,0 +1,22 @@
+import { TableCell } from "@tremor/react";
+import { Team } from "@/components/key_team_helpers/key_list";
+import TeamRoleBadge from "@/app/(dashboard)/teams/components/TeamsTable/YourRoleCell/TeamRoleBadge";
+
+interface YourRoleCellProps {
+ team: Team;
+ userId: string | null;
+}
+
+const getUserRole = (team: Team, userId: string | null): string | null => {
+ if (!userId) return null;
+ const member = team.members_with_roles?.find((m) => m.user_id === userId);
+ return member?.role ?? null;
+};
+
+const YourRoleCell = ({ team, userId }: YourRoleCellProps) => {
+ const roleBadge = TeamRoleBadge(getUserRole(team, userId));
+
+ return {roleBadge};
+};
+
+export default YourRoleCell;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/CreateTeamModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/CreateTeamModal.tsx
new file mode 100644
index 00000000000..e6d9a85898e
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/CreateTeamModal.tsx
@@ -0,0 +1,573 @@
+import { Button as Button2, Form, Input, Modal, Select as Select2, Tooltip } from "antd";
+import { Accordion, AccordionBody, AccordionHeader, Text, TextInput } from "@tremor/react";
+import { InfoCircleOutlined } from "@ant-design/icons";
+import {
+ fetchAvailableModelsForTeamOrKey,
+ getModelDisplayName,
+ unfurlWildcardModelsInList,
+} from "@/components/key_team_helpers/fetch_available_models_team_key";
+import NumericalInput from "@/components/shared/numerical_input";
+import VectorStoreSelector from "@/components/vector_store_management/VectorStoreSelector";
+import MCPServerSelector from "@/components/mcp_server_management/MCPServerSelector";
+import PremiumLoggingSettings from "@/components/common_components/PremiumLoggingSettings";
+import ModelAliasManager from "@/components/common_components/ModelAliasManager";
+import React, { useEffect, useState } from "react";
+import NotificationsManager from "@/components/molecules/notifications_manager";
+import { fetchMCPAccessGroups, getGuardrailsList, Organization, Team, teamCreateCall } from "@/components/networking";
+import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
+import MCPToolPermissions from "@/components/mcp_server_management/MCPToolPermissions";
+
+interface ModelAliases {
+ [key: string]: string;
+}
+
+interface CreateTeamModalProps {
+ isTeamModalVisible: boolean;
+ handleOk: () => void;
+ handleCancel: () => void;
+ currentOrg: Organization | null;
+ organizations: Organization[] | null;
+ teams: Team[] | null;
+ setTeams: (teams: Team[] | null) => void;
+ modelAliases: ModelAliases;
+ setModelAliases: (modelAliases: ModelAliases) => void;
+ loggingSettings: any[];
+ setLoggingSettings: (loggingSettings: any[]) => void;
+ setIsTeamModalVisible: (isTeamModalVisible: boolean) => void;
+}
+
+const getOrganizationModels = (organization: Organization | null, userModels: string[]) => {
+ let tempModelsToPick = [];
+
+ if (organization) {
+ if (organization.models.length > 0) {
+ console.log(`organization.models: ${organization.models}`);
+ tempModelsToPick = organization.models;
+ } else {
+ // show all available models if the team has no models set
+ tempModelsToPick = userModels;
+ }
+ } else {
+ // no team set, show all available models
+ tempModelsToPick = userModels;
+ }
+
+ return unfurlWildcardModelsInList(tempModelsToPick, userModels);
+};
+
+const CreateTeamModal = ({
+ isTeamModalVisible,
+ handleOk,
+ handleCancel,
+ currentOrg,
+ organizations,
+ teams,
+ setTeams,
+ modelAliases,
+ setModelAliases,
+ loggingSettings,
+ setLoggingSettings,
+ setIsTeamModalVisible,
+}: CreateTeamModalProps) => {
+ const { userId: userID, userRole, accessToken, premiumUser } = useAuthorized();
+ const [form] = Form.useForm();
+ const [userModels, setUserModels] = useState([]);
+ const [currentOrgForCreateTeam, setCurrentOrgForCreateTeam] = useState(null);
+ const [modelsToPick, setModelsToPick] = useState([]);
+ const [guardrailsList, setGuardrailsList] = useState([]);
+ const [mcpAccessGroups, setMcpAccessGroups] = useState([]);
+ const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false);
+
+ useEffect(() => {
+ const fetchUserModels = async () => {
+ try {
+ if (userID === null || userRole === null || accessToken === null) {
+ return;
+ }
+ const models = await fetchAvailableModelsForTeamOrKey(userID, userRole, accessToken);
+ if (models) {
+ setUserModels(models);
+ }
+ } catch (error) {
+ console.error("Error fetching user models:", error);
+ }
+ };
+
+ fetchUserModels();
+ }, [accessToken, userID, userRole, teams]);
+
+ useEffect(() => {
+ console.log(`currentOrgForCreateTeam: ${currentOrgForCreateTeam}`);
+ const models = getOrganizationModels(currentOrgForCreateTeam, userModels);
+ console.log(`models: ${models}`);
+ setModelsToPick(models);
+ form.setFieldValue("models", []);
+ }, [currentOrgForCreateTeam, userModels]);
+
+ useEffect(() => {
+ fetchMcpAccessGroups();
+ }, [accessToken]);
+
+ useEffect(() => {
+ const fetchGuardrails = async () => {
+ try {
+ if (accessToken == null) {
+ return;
+ }
+
+ const response = await getGuardrailsList(accessToken);
+ const guardrailNames = response.guardrails.map((g: { guardrail_name: string }) => g.guardrail_name);
+ setGuardrailsList(guardrailNames);
+ } catch (error) {
+ console.error("Failed to fetch guardrails:", error);
+ }
+ };
+
+ fetchGuardrails();
+ }, [accessToken]);
+
+ const fetchMcpAccessGroups = async () => {
+ try {
+ if (accessToken == null) {
+ return;
+ }
+ const groups = await fetchMCPAccessGroups(accessToken);
+ setMcpAccessGroups(groups);
+ } catch (error) {
+ console.error("Failed to fetch MCP access groups:", error);
+ }
+ };
+
+ const handleCreate = async (formValues: Record) => {
+ try {
+ console.log(`formValues: ${JSON.stringify(formValues)}`);
+ if (accessToken != null) {
+ const newTeamAlias = formValues?.team_alias;
+ const existingTeamAliases = teams?.map((t) => t.team_alias) ?? [];
+ let organizationId = formValues?.organization_id || currentOrg?.organization_id;
+ if (organizationId === "" || typeof organizationId !== "string") {
+ formValues.organization_id = null;
+ } else {
+ formValues.organization_id = organizationId.trim();
+ }
+
+ // Remove guardrails from top level since it's now in metadata
+ if (existingTeamAliases.includes(newTeamAlias)) {
+ throw new Error(`Team alias ${newTeamAlias} already exists, please pick another alias`);
+ }
+
+ NotificationsManager.info("Creating Team");
+
+ // Handle logging settings in metadata
+ if (loggingSettings.length > 0) {
+ let metadata = {};
+ if (formValues.metadata) {
+ try {
+ metadata = JSON.parse(formValues.metadata);
+ } catch (e) {
+ console.warn("Invalid JSON in metadata field, starting with empty object");
+ }
+ }
+
+ // Add logging settings to metadata
+ metadata = {
+ ...metadata,
+ logging: loggingSettings.filter((config) => config.callback_name), // Only include configs with callback_name
+ };
+
+ formValues.metadata = JSON.stringify(metadata);
+ }
+
+ // Transform allowed_vector_store_ids and allowed_mcp_servers_and_groups into object_permission
+ if (
+ (formValues.allowed_vector_store_ids && formValues.allowed_vector_store_ids.length > 0) ||
+ (formValues.allowed_mcp_servers_and_groups &&
+ (formValues.allowed_mcp_servers_and_groups.servers?.length > 0 ||
+ formValues.allowed_mcp_servers_and_groups.accessGroups?.length > 0 ||
+ formValues.allowed_mcp_servers_and_groups.toolPermissions))
+ ) {
+ formValues.object_permission = {};
+ if (formValues.allowed_vector_store_ids && formValues.allowed_vector_store_ids.length > 0) {
+ formValues.object_permission.vector_stores = formValues.allowed_vector_store_ids;
+ delete formValues.allowed_vector_store_ids;
+ }
+ if (formValues.allowed_mcp_servers_and_groups) {
+ const { servers, accessGroups } = formValues.allowed_mcp_servers_and_groups;
+ if (servers && servers.length > 0) {
+ formValues.object_permission.mcp_servers = servers;
+ }
+ if (accessGroups && accessGroups.length > 0) {
+ formValues.object_permission.mcp_access_groups = accessGroups;
+ }
+ delete formValues.allowed_mcp_servers_and_groups;
+ }
+
+ // Add tool permissions separately
+ if (formValues.mcp_tool_permissions && Object.keys(formValues.mcp_tool_permissions).length > 0) {
+ if (!formValues.object_permission) {
+ formValues.object_permission = {};
+ }
+ formValues.object_permission.mcp_tool_permissions = formValues.mcp_tool_permissions;
+ delete formValues.mcp_tool_permissions;
+ }
+ }
+
+ // Transform allowed_mcp_access_groups into object_permission
+ if (formValues.allowed_mcp_access_groups && formValues.allowed_mcp_access_groups.length > 0) {
+ if (!formValues.object_permission) {
+ formValues.object_permission = {};
+ }
+ formValues.object_permission.mcp_access_groups = formValues.allowed_mcp_access_groups;
+ delete formValues.allowed_mcp_access_groups;
+ }
+
+ // Add model_aliases if any are defined
+ if (Object.keys(modelAliases).length > 0) {
+ formValues.model_aliases = modelAliases;
+ }
+
+ const response: any = await teamCreateCall(accessToken, formValues);
+ if (teams !== null) {
+ setTeams([...teams, response]);
+ } else {
+ setTeams([response]);
+ }
+ console.log(`response for team create call: ${response}`);
+ NotificationsManager.success("Team created");
+ form.resetFields();
+ setLoggingSettings([]);
+ setModelAliases({});
+ setIsTeamModalVisible(false);
+ }
+ } catch (error) {
+ console.error("Error creating the team:", error);
+ NotificationsManager.fromBackend("Error creating the team: " + error);
+ }
+ };
+
+ return (
+
+
+
+
+
+ Organization{" "}
+
+ Organizations can have multiple teams. Learn more about{" "}
+ e.stopPropagation()}
+ >
+ user management hierarchy
+
+
+ }
+ >
+
+
+
+ }
+ name="organization_id"
+ initialValue={currentOrg ? currentOrg.organization_id : null}
+ className="mt-8"
+ >
+ {
+ form.setFieldValue("organization_id", value);
+ setCurrentOrgForCreateTeam(organizations?.find((org) => org.organization_id === value) || null);
+ }}
+ filterOption={(input, option) => {
+ if (!option) return false;
+ const optionValue = option.children?.toString() || "";
+ return optionValue.toLowerCase().includes(input.toLowerCase());
+ }}
+ optionFilterProp="children"
+ >
+ {organizations?.map((org) => (
+
+ {org.organization_alias}{" "}
+ ({org.organization_id})
+
+ ))}
+
+
+
+ Models{" "}
+
+
+
+
+ }
+ name="models"
+ >
+
+
+ All Proxy Models
+
+ {modelsToPick.map((model) => (
+
+ {getModelDisplayName(model)}
+
+ ))}
+
+
+
+
+
+
+
+
+ daily
+ weekly
+ monthly
+
+
+
+
+
+
+
+
+
+ {
+ if (!mcpAccessGroupsLoaded) {
+ fetchMcpAccessGroups();
+ setMcpAccessGroupsLoaded(true);
+ }
+ }}
+ >
+
+ Additional Settings
+
+
+
+ {
+ e.target.value = e.target.value.trim();
+ }}
+ />
+
+ (value ? Number(value) : undefined)}
+ tooltip="This is the individual budget for a user in the team."
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Guardrails{" "}
+
+ e.stopPropagation()}
+ >
+
+
+
+
+ }
+ name="guardrails"
+ className="mt-8"
+ help="Select existing guardrails or enter new ones"
+ >
+ ({
+ value: name,
+ label: name,
+ }))}
+ />
+
+
+ Allowed Vector Stores{" "}
+
+
+
+
+ }
+ name="allowed_vector_store_ids"
+ className="mt-8"
+ help="Select vector stores this team can access. Leave empty for access to all vector stores"
+ >
+ form.setFieldValue("allowed_vector_store_ids", values)}
+ value={form.getFieldValue("allowed_vector_store_ids")}
+ accessToken={accessToken || ""}
+ placeholder="Select vector stores (optional)"
+ />
+
+
+
+
+
+
+ MCP Settings
+
+
+
+ Allowed MCP Servers{" "}
+
+
+
+
+ }
+ name="allowed_mcp_servers_and_groups"
+ className="mt-4"
+ help="Select MCP servers or access groups this team can access"
+ >
+ form.setFieldValue("allowed_mcp_servers_and_groups", val)}
+ value={form.getFieldValue("allowed_mcp_servers_and_groups")}
+ accessToken={accessToken || ""}
+ placeholder="Select MCP servers or access groups (optional)"
+ />
+
+
+ {/* Hidden field to register mcp_tool_permissions with the form */}
+
+
+
+
+
+ prevValues.allowed_mcp_servers_and_groups !== currentValues.allowed_mcp_servers_and_groups ||
+ prevValues.mcp_tool_permissions !== currentValues.mcp_tool_permissions
+ }
+ >
+ {() => (
+
+ form.setFieldsValue({ mcp_tool_permissions: toolPerms })}
+ />
+
+ )}
+
+
+
+
+
+
+ Logging Settings
+
+
+
+
+
+
+
+
+ Model Aliases
+
+
+
+
+ Create custom aliases for models that can be used by team members in API calls. This allows you to
+ create shortcuts for specific models.
+
+
+
+
+
+ >
+
+ Create Team
+
+
+
+ );
+};
+
+export default CreateTeamModal;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/DeleteTeamModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/DeleteTeamModal.tsx
new file mode 100644
index 00000000000..0be627fdfc4
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/components/modals/DeleteTeamModal.tsx
@@ -0,0 +1,95 @@
+import { AlertTriangleIcon, XIcon } from "lucide-react";
+import React, { useState } from "react";
+import { Team } from "@/components/key_team_helpers/key_list";
+
+interface DeleteTeamModalProps {
+ teams: Team[] | null;
+ teamToDelete: string | null;
+ onCancel: () => void;
+ onConfirm: () => void;
+}
+
+const DeleteTeamModal = ({ teams, teamToDelete, onCancel, onConfirm }: DeleteTeamModalProps) => {
+ const [deleteConfirmInput, setDeleteConfirmInput] = useState("");
+
+ const team = teams?.find((t) => t.team_id === teamToDelete);
+ const teamName = team?.team_alias || "";
+ const keyCount = team?.keys?.length || 0;
+ const isValid = deleteConfirmInput === teamName;
+
+ return (
+
+
+
+
+
Delete Team
+
+
+
+ {keyCount > 0 && (
+
+
+
+
+ Warning: This team has {keyCount} associated key{keyCount > 1 ? "s" : ""}.
+
+
+ Deleting the team will also delete all associated keys. This action is irreversible.
+
+
+
+ )}
+
+ Are you sure you want to force delete this team and all its keys?
+
+
+
+ setDeleteConfirmInput(e.target.value)}
+ placeholder="Enter team name exactly"
+ className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-base"
+ autoFocus
+ />
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default DeleteTeamModal;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/hooks/useFetchTeams.ts b/ui/litellm-dashboard/src/app/(dashboard)/teams/hooks/useFetchTeams.ts
new file mode 100644
index 00000000000..c02787896f9
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/hooks/useFetchTeams.ts
@@ -0,0 +1,30 @@
+import { useCallback, useEffect, useState } from "react";
+import { fetchTeams } from "@/components/common_components/fetch_teams";
+import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
+import { Organization, Team } from "@/components/networking";
+
+interface useFetchTeamsProps {
+ currentOrg: Organization | null;
+ setTeams: (teams: Team[] | null) => void;
+}
+
+const useFetchTeams = ({ currentOrg, setTeams }: useFetchTeamsProps) => {
+ const [lastRefreshed, setLastRefreshed] = useState("");
+ const { accessToken, userId, userRole } = useAuthorized();
+
+ const onRefreshClick = useCallback(() => {
+ const currentDate = new Date();
+ setLastRefreshed(currentDate.toLocaleString());
+ }, []);
+
+ useEffect(() => {
+ if (accessToken) {
+ fetchTeams(accessToken, userId, userRole, currentOrg, setTeams).then();
+ }
+ onRefreshClick();
+ }, [accessToken, currentOrg, lastRefreshed, onRefreshClick, setTeams, userId, userRole]);
+
+ return { lastRefreshed, setLastRefreshed, onRefreshClick };
+};
+
+export default useFetchTeams;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/page.tsx
index 42e6d250ee4..041c50dd32a 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/teams/page.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import Teams from "@/components/teams";
+import TeamsView from "@/app/(dashboard)/teams/TeamsView";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import useTeams from "@/app/(dashboard)/hooks/useTeams";
import { useEffect, useState } from "react";
@@ -10,9 +10,6 @@ import { fetchOrganizations } from "@/components/organizations";
const TeamsPage = () => {
const { accessToken, userId, userRole } = useAuthorized();
const { teams, setTeams } = useTeams();
- const [searchParams, setSearchParams] = useState(() =>
- typeof window === "undefined" ? new URLSearchParams() : new URLSearchParams(window.location.search),
- );
const [organizations, setOrganizations] = useState([]);
useEffect(() => {
@@ -20,9 +17,8 @@ const TeamsPage = () => {
}, [accessToken]);
return (
-
) : page == "teams" ? (
- >;
- userID: string | null;
- userRole: string | null;
- organizations: Organization[] | null;
- premiumUser?: boolean;
-}
-
-interface FilterState {
- team_id: string;
- team_alias: string;
- organization_id: string;
- sort_by: string;
- sort_order: "asc" | "desc";
-}
-
-interface EditTeamModalProps {
- visible: boolean;
- onCancel: () => void;
- team: any; // Assuming TeamType is a type representing your team object
- onSubmit: (data: FormData) => void; // Assuming FormData is the type of data to be submitted
-}
-
-import {
- teamCreateCall,
- teamMemberAddCall,
- teamMemberUpdateCall,
- Member,
- modelAvailableCall,
- v2TeamListCall,
-} from "./networking";
-import { updateExistingKeys } from "@/utils/dataUtils";
-
-interface TeamInfo {
- members_with_roles: Member[];
-}
-
-interface PerTeamInfo {
- keys: KeyResponse[];
- team_info: TeamInfo;
-}
-
-const getOrganizationModels = (organization: Organization | null, userModels: string[]) => {
- let tempModelsToPick = [];
-
- if (organization) {
- if (organization.models.length > 0) {
- console.log(`organization.models: ${organization.models}`);
- tempModelsToPick = organization.models;
- } else {
- // show all available models if the team has no models set
- tempModelsToPick = userModels;
- }
- } else {
- // no team set, show all available models
- tempModelsToPick = userModels;
- }
-
- return unfurlWildcardModelsInList(tempModelsToPick, userModels);
-};
-
-const Teams: React.FC = ({
- teams,
- searchParams,
- accessToken,
- setTeams,
- userID,
- userRole,
- organizations,
- premiumUser = false,
-}) => {
- const [lastRefreshed, setLastRefreshed] = useState("");
- const [currentOrg, setCurrentOrg] = useState(null);
- const [currentOrgForCreateTeam, setCurrentOrgForCreateTeam] = useState(null);
- const [showFilters, setShowFilters] = useState(false);
- const [filters, setFilters] = useState({
- team_id: "",
- team_alias: "",
- organization_id: "",
- sort_by: "created_at",
- sort_order: "desc",
- });
-
- useEffect(() => {
- console.log(`inside useeffect - ${lastRefreshed}`);
- if (accessToken) {
- // Call your function here
- fetchTeams(accessToken, userID, userRole, currentOrg, setTeams);
- }
- handleRefreshClick();
- }, [lastRefreshed]);
-
- const [form] = Form.useForm();
- const [memberForm] = Form.useForm();
- const { Title, Paragraph } = Typography;
- const [value, setValue] = useState("");
- const [editModalVisible, setEditModalVisible] = useState(false);
-
- const [selectedTeam, setSelectedTeam] = useState(null);
- const [selectedTeamId, setSelectedTeamId] = useState(null);
- const [editTeam, setEditTeam] = useState(false);
-
- const [isTeamModalVisible, setIsTeamModalVisible] = useState(false);
- const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false);
- const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false);
- const [userModels, setUserModels] = useState([]);
- const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
- const [teamToDelete, setTeamToDelete] = useState(null);
- const [modelsToPick, setModelsToPick] = useState([]);
- const [perTeamInfo, setPerTeamInfo] = useState>({});
-
- // Add this state near the other useState declarations
- const [guardrailsList, setGuardrailsList] = useState([]);
- const [expandedAccordions, setExpandedAccordions] = useState>({});
- const [loggingSettings, setLoggingSettings] = useState([]);
- const [mcpAccessGroups, setMcpAccessGroups] = useState([]);
- const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false);
- const [deleteConfirmInput, setDeleteConfirmInput] = useState("");
- const [modelAliases, setModelAliases] = useState<{ [key: string]: string }>({});
-
- useEffect(() => {
- console.log(`currentOrgForCreateTeam: ${currentOrgForCreateTeam}`);
- const models = getOrganizationModels(currentOrgForCreateTeam, userModels);
- console.log(`models: ${models}`);
- setModelsToPick(models);
- form.setFieldValue("models", []);
- }, [currentOrgForCreateTeam, userModels]);
-
- // Add this useEffect to fetch guardrails
- useEffect(() => {
- const fetchGuardrails = async () => {
- try {
- if (accessToken == null) {
- return;
- }
-
- const response = await getGuardrailsList(accessToken);
- const guardrailNames = response.guardrails.map((g: { guardrail_name: string }) => g.guardrail_name);
- setGuardrailsList(guardrailNames);
- } catch (error) {
- console.error("Failed to fetch guardrails:", error);
- }
- };
-
- fetchGuardrails();
- }, [accessToken]);
-
- const fetchMcpAccessGroups = async () => {
- try {
- if (accessToken == null) {
- return;
- }
- const groups = await fetchMCPAccessGroups(accessToken);
- setMcpAccessGroups(groups);
- } catch (error) {
- console.error("Failed to fetch MCP access groups:", error);
- }
- };
-
- useEffect(() => {
- fetchMcpAccessGroups();
- }, [accessToken]);
-
- useEffect(() => {
- const fetchTeamInfo = () => {
- if (!teams) return;
-
- const newPerTeamInfo = teams.reduce(
- (acc, team) => {
- acc[team.team_id] = {
- keys: team.keys || [],
- team_info: {
- members_with_roles: team.members_with_roles || [],
- },
- };
- return acc;
- },
- {} as Record,
- );
-
- setPerTeamInfo(newPerTeamInfo);
- };
-
- fetchTeamInfo();
- }, [teams]);
-
- const handleOk = () => {
- setIsTeamModalVisible(false);
- form.resetFields();
- setLoggingSettings([]);
- setModelAliases({});
- };
-
- const handleMemberOk = () => {
- setIsAddMemberModalVisible(false);
- setIsEditMemberModalVisible(false);
- memberForm.resetFields();
- };
-
- const handleCancel = () => {
- setIsTeamModalVisible(false);
- form.resetFields();
- setLoggingSettings([]);
- setModelAliases({});
- };
-
- const handleMemberCancel = () => {
- setIsAddMemberModalVisible(false);
- setIsEditMemberModalVisible(false);
- memberForm.resetFields();
- };
-
- const handleDelete = async (team_id: string) => {
- // Set the team to delete and open the confirmation modal
- setTeamToDelete(team_id);
- setIsDeleteModalOpen(true);
- };
-
- const confirmDelete = async () => {
- if (teamToDelete == null || teams == null || accessToken == null) {
- return;
- }
-
- try {
- await teamDeleteCall(accessToken, teamToDelete);
- // Successfully completed the deletion. Update the state to trigger a rerender.
- fetchTeams(accessToken, userID, userRole, currentOrg, setTeams);
- } catch (error) {
- console.error("Error deleting the team:", error);
- // Handle any error situations, such as displaying an error message to the user.
- }
-
- // Close the confirmation modal and reset the teamToDelete
- setIsDeleteModalOpen(false);
- setTeamToDelete(null);
- };
-
- const cancelDelete = () => {
- // Close the confirmation modal and reset the teamToDelete
- setIsDeleteModalOpen(false);
- setTeamToDelete(null);
- };
-
- useEffect(() => {
- const fetchUserModels = async () => {
- try {
- if (userID === null || userRole === null || accessToken === null) {
- return;
- }
- const models = await fetchAvailableModelsForTeamOrKey(userID, userRole, accessToken);
- if (models) {
- setUserModels(models);
- }
- } catch (error) {
- console.error("Error fetching user models:", error);
- }
- };
-
- fetchUserModels();
- }, [accessToken, userID, userRole, teams]);
-
- const handleCreate = async (formValues: Record) => {
- try {
- console.log(`formValues: ${JSON.stringify(formValues)}`);
- if (accessToken != null) {
- const newTeamAlias = formValues?.team_alias;
- const existingTeamAliases = teams?.map((t) => t.team_alias) ?? [];
- let organizationId = formValues?.organization_id || currentOrg?.organization_id;
- if (organizationId === "" || typeof organizationId !== "string") {
- formValues.organization_id = null;
- } else {
- formValues.organization_id = organizationId.trim();
- }
-
- // Remove guardrails from top level since it's now in metadata
- if (existingTeamAliases.includes(newTeamAlias)) {
- throw new Error(`Team alias ${newTeamAlias} already exists, please pick another alias`);
- }
-
- NotificationsManager.info("Creating Team");
-
- // Handle logging settings in metadata
- if (loggingSettings.length > 0) {
- let metadata = {};
- if (formValues.metadata) {
- try {
- metadata = JSON.parse(formValues.metadata);
- } catch (e) {
- console.warn("Invalid JSON in metadata field, starting with empty object");
- }
- }
-
- // Add logging settings to metadata
- metadata = {
- ...metadata,
- logging: loggingSettings.filter((config) => config.callback_name), // Only include configs with callback_name
- };
-
- formValues.metadata = JSON.stringify(metadata);
- }
-
- // Transform allowed_vector_store_ids and allowed_mcp_servers_and_groups into object_permission
- if (
- (formValues.allowed_vector_store_ids && formValues.allowed_vector_store_ids.length > 0) ||
- (formValues.allowed_mcp_servers_and_groups &&
- (formValues.allowed_mcp_servers_and_groups.servers?.length > 0 ||
- formValues.allowed_mcp_servers_and_groups.accessGroups?.length > 0 ||
- formValues.allowed_mcp_servers_and_groups.toolPermissions))
- ) {
- formValues.object_permission = {};
- if (formValues.allowed_vector_store_ids && formValues.allowed_vector_store_ids.length > 0) {
- formValues.object_permission.vector_stores = formValues.allowed_vector_store_ids;
- delete formValues.allowed_vector_store_ids;
- }
- if (formValues.allowed_mcp_servers_and_groups) {
- const { servers, accessGroups } = formValues.allowed_mcp_servers_and_groups;
- if (servers && servers.length > 0) {
- formValues.object_permission.mcp_servers = servers;
- }
- if (accessGroups && accessGroups.length > 0) {
- formValues.object_permission.mcp_access_groups = accessGroups;
- }
- delete formValues.allowed_mcp_servers_and_groups;
- }
-
- // Add tool permissions separately
- if (formValues.mcp_tool_permissions && Object.keys(formValues.mcp_tool_permissions).length > 0) {
- if (!formValues.object_permission) {
- formValues.object_permission = {};
- }
- formValues.object_permission.mcp_tool_permissions = formValues.mcp_tool_permissions;
- delete formValues.mcp_tool_permissions;
- }
- }
-
- // Transform allowed_mcp_access_groups into object_permission
- if (formValues.allowed_mcp_access_groups && formValues.allowed_mcp_access_groups.length > 0) {
- if (!formValues.object_permission) {
- formValues.object_permission = {};
- }
- formValues.object_permission.mcp_access_groups = formValues.allowed_mcp_access_groups;
- delete formValues.allowed_mcp_access_groups;
- }
-
- // Add model_aliases if any are defined
- if (Object.keys(modelAliases).length > 0) {
- formValues.model_aliases = modelAliases;
- }
-
- const response: any = await teamCreateCall(accessToken, formValues);
- if (teams !== null) {
- setTeams([...teams, response]);
- } else {
- setTeams([response]);
- }
- console.log(`response for team create call: ${response}`);
- NotificationsManager.success("Team created");
- form.resetFields();
- setLoggingSettings([]);
- setModelAliases({});
- setIsTeamModalVisible(false);
- }
- } catch (error) {
- console.error("Error creating the team:", error);
- NotificationsManager.fromBackend("Error creating the team: " + error);
- }
- };
-
- const is_team_admin = (team: any) => {
- if (team == null || team.members_with_roles == null) {
- return false;
- }
- for (let i = 0; i < team.members_with_roles.length; i++) {
- let member = team.members_with_roles[i];
- if (member.user_id == userID && member.role == "admin") {
- return true;
- }
- }
- return false;
- };
-
- const handleRefreshClick = () => {
- // Update the 'lastRefreshed' state to the current date and time
- const currentDate = new Date();
- setLastRefreshed(currentDate.toLocaleString());
- };
-
- const handleFilterChange = (key: keyof FilterState, value: string) => {
- const newFilters = { ...filters, [key]: value };
- setFilters(newFilters);
- // Call teamListCall with the new filters
- if (accessToken) {
- v2TeamListCall(
- accessToken,
- newFilters.organization_id || null,
- null,
- newFilters.team_id || null,
- newFilters.team_alias || null,
- )
- .then((response) => {
- if (response && response.teams) {
- setTeams(response.teams);
- }
- })
- .catch((error) => {
- console.error("Error fetching teams:", error);
- });
- }
- };
-
- const handleSortChange = (sortBy: string, sortOrder: "asc" | "desc") => {
- const newFilters = {
- ...filters,
- sort_by: sortBy,
- sort_order: sortOrder,
- };
- setFilters(newFilters);
- // Call teamListCall with the new sort parameters
- if (accessToken) {
- v2TeamListCall(
- accessToken,
- filters.organization_id || null,
- null,
- filters.team_id || null,
- filters.team_alias || null,
- )
- .then((response) => {
- if (response && response.teams) {
- setTeams(response.teams);
- }
- })
- .catch((error) => {
- console.error("Error fetching teams:", error);
- });
- }
- };
-
- const handleFilterReset = () => {
- setFilters({
- team_id: "",
- team_alias: "",
- organization_id: "",
- sort_by: "created_at",
- sort_order: "desc",
- });
- // Reset teams list
- if (accessToken) {
- v2TeamListCall(accessToken, null, userID || null, null, null)
- .then((response) => {
- if (response && response.teams) {
- setTeams(response.teams);
- }
- })
- .catch((error) => {
- console.error("Error fetching teams:", error);
- });
- }
- };
-
- return (
-
-
-
- {(userRole == "Admin" || userRole == "Org Admin") && (
-
- )}
- {selectedTeamId ? (
- {
- setTeams((teams) => {
- if (teams == null) {
- return teams;
- }
- const updated = teams.map((team) => {
- if (data.team_id === team.team_id) {
- return updateExistingKeys(team, data);
- }
- return team;
- });
- // Minimal fix: refresh the full team list after an update
- if (accessToken) {
- fetchTeams(accessToken, userID, userRole, currentOrg, setTeams);
- }
- return updated;
- });
- }}
- onClose={() => {
- setSelectedTeamId(null);
- setEditTeam(false);
- }}
- accessToken={accessToken}
- is_team_admin={is_team_admin(teams?.find((team) => team.team_id === selectedTeamId))}
- is_proxy_admin={userRole == "Admin"}
- userModels={userModels}
- editTeam={editTeam}
- />
- ) : (
-
-
-
- Your Teams
- Available Teams
- {isAdminRole(userRole || "") && Default Team Settings}
-
-
- {lastRefreshed && Last Refreshed: {lastRefreshed}}
-
-
-
-
-
-
- Click on “Team ID” to view team details and manage team members.
-
-
-
-
-
-
- {/* Search and Filter Controls */}
-
- {/* Team Alias Search */}
-
-
handleFilterChange("team_alias", e.target.value)}
- />
-
-
-
- {/* Filter Button */}
-
-
- {/* Reset Filters Button */}
-
-
-
- {/* Additional Filters */}
- {showFilters && (
-
- {/* Team ID Search */}
-
-
handleFilterChange("team_id", e.target.value)}
- />
-
-
-
- {/* Organization Dropdown */}
-
-
-
-
- )}
-
-
-
-
-
- Team Name
- Team ID
- Created
- Spend (USD)
- Budget (USD)
- Models
- Organization
- Info
-
-
-
-
- {teams && teams.length > 0
- ? teams
- .filter((team) => {
- if (!currentOrg) return true;
- return team.organization_id === currentOrg.organization_id;
- })
- .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
- .map((team: any) => (
-
-
- {team["team_alias"]}
-
-
-
-
-
-
-
-
-
- {team.created_at ? new Date(team.created_at).toLocaleDateString() : "N/A"}
-
-
- {formatNumberWithCommas(team["spend"], 4)}
-
-
- {team["max_budget"] !== null && team["max_budget"] !== undefined
- ? team["max_budget"]
- : "No limit"}
-
- 3 ? "px-0" : ""}
- >
-
- {Array.isArray(team.models) ? (
-
- {team.models.length === 0 ? (
-
- All Proxy Models
-
- ) : (
- <>
-
- {team.models.length > 3 && (
-
- {
- setExpandedAccordions((prev) => ({
- ...prev,
- [team.team_id]: !prev[team.team_id],
- }));
- }}
- />
-
- )}
-
- {team.models.slice(0, 3).map((model: string, index: number) =>
- model === "all-proxy-models" ? (
-
- All Proxy Models
-
- ) : (
-
-
- {model.length > 30
- ? `${getModelDisplayName(model).slice(0, 30)}...`
- : getModelDisplayName(model)}
-
-
- ),
- )}
- {team.models.length > 3 && !expandedAccordions[team.team_id] && (
-
-
- +{team.models.length - 3}{" "}
- {team.models.length - 3 === 1
- ? "more model"
- : "more models"}
-
-
- )}
- {expandedAccordions[team.team_id] && (
-
- {team.models.slice(3).map((model: string, index: number) =>
- model === "all-proxy-models" ? (
-
- All Proxy Models
-
- ) : (
-
-
- {model.length > 30
- ? `${getModelDisplayName(model).slice(0, 30)}...`
- : getModelDisplayName(model)}
-
-
- ),
- )}
-
- )}
-
-
- >
- )}
-
- ) : null}
-
-
-
- {team.organization_id}
-
-
- {perTeamInfo &&
- team.team_id &&
- perTeamInfo[team.team_id] &&
- perTeamInfo[team.team_id].keys &&
- perTeamInfo[team.team_id].keys.length}{" "}
- Keys
-
-
- {perTeamInfo &&
- team.team_id &&
- perTeamInfo[team.team_id] &&
- perTeamInfo[team.team_id].team_info &&
- perTeamInfo[team.team_id].team_info.members_with_roles &&
- perTeamInfo[team.team_id].team_info.members_with_roles.length}{" "}
- Members
-
-
-
- {userRole == "Admin" ? (
- <>
- {
- setSelectedTeamId(team.team_id);
- setEditTeam(true);
- }}
- />
- handleDelete(team.team_id)}
- icon={TrashIcon}
- size="sm"
- />
- >
- ) : null}
-
-
- ))
- : null}
-
-
- {isDeleteModalOpen &&
- (() => {
- const team = teams?.find((t) => t.team_id === teamToDelete);
- const teamName = team?.team_alias || "";
- const keyCount = team?.keys?.length || 0;
- const isValid = deleteConfirmInput === teamName;
- return (
-
-
-
-
-
Delete Team
-
-
-
- {keyCount > 0 && (
-
-
-
-
- Warning: This team has {keyCount} associated key{keyCount > 1 ? "s" : ""}.
-
-
- Deleting the team will also delete all associated keys. This action is
- irreversible.
-
-
-
- )}
-
- Are you sure you want to force delete this team and all its keys?
-
-
-
- setDeleteConfirmInput(e.target.value)}
- placeholder="Enter team name exactly"
- className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-base"
- autoFocus
- />
-
-
-
-
-
-
-
-
-
- );
- })()}
-
-
-
-
-
-
-
- {isAdminRole(userRole || "") && (
-
-
-
- )}
-
-
- )}
- {(userRole == "Admin" || userRole == "Org Admin") && (
-
-
-
-
-
- Organization{" "}
-
- Organizations can have multiple teams. Learn more about{" "}
- e.stopPropagation()}
- >
- user management hierarchy
-
-
- }
- >
-
-
-
- }
- name="organization_id"
- initialValue={currentOrg ? currentOrg.organization_id : null}
- className="mt-8"
- >
- {
- form.setFieldValue("organization_id", value);
- setCurrentOrgForCreateTeam(organizations?.find((org) => org.organization_id === value) || null);
- }}
- filterOption={(input, option) => {
- if (!option) return false;
- const optionValue = option.children?.toString() || "";
- return optionValue.toLowerCase().includes(input.toLowerCase());
- }}
- optionFilterProp="children"
- >
- {organizations?.map((org) => (
-
- {org.organization_alias}{" "}
- ({org.organization_id})
-
- ))}
-
-
-
- Models{" "}
-
-
-
-
- }
- name="models"
- >
-
-
- All Proxy Models
-
- {modelsToPick.map((model) => (
-
- {getModelDisplayName(model)}
-
- ))}
-
-
-
-
-
-
-
-
- daily
- weekly
- monthly
-
-
-
-
-
-
-
-
-
- {
- if (!mcpAccessGroupsLoaded) {
- fetchMcpAccessGroups();
- setMcpAccessGroupsLoaded(true);
- }
- }}
- >
-
- Additional Settings
-
-
-
- {
- e.target.value = e.target.value.trim();
- }}
- />
-
- (value ? Number(value) : undefined)}
- tooltip="This is the individual budget for a user in the team."
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Guardrails{" "}
-
- e.stopPropagation()}
- >
-
-
-
-
- }
- name="guardrails"
- className="mt-8"
- help="Select existing guardrails or enter new ones"
- >
- ({
- value: name,
- label: name,
- }))}
- />
-
-
- Allowed Vector Stores{" "}
-
-
-
-
- }
- name="allowed_vector_store_ids"
- className="mt-8"
- help="Select vector stores this team can access. Leave empty for access to all vector stores"
- >
- form.setFieldValue("allowed_vector_store_ids", values)}
- value={form.getFieldValue("allowed_vector_store_ids")}
- accessToken={accessToken || ""}
- placeholder="Select vector stores (optional)"
- />
-
-
-
-
-
-
- MCP Settings
-
-
-
- Allowed MCP Servers{" "}
-
-
-
-
- }
- name="allowed_mcp_servers_and_groups"
- className="mt-4"
- help="Select MCP servers or access groups this team can access"
- >
- form.setFieldValue("allowed_mcp_servers_and_groups", val)}
- value={form.getFieldValue("allowed_mcp_servers_and_groups")}
- accessToken={accessToken || ""}
- placeholder="Select MCP servers or access groups (optional)"
- />
-
-
- {/* Hidden field to register mcp_tool_permissions with the form */}
-
-
-
-
-
- prevValues.allowed_mcp_servers_and_groups !== currentValues.allowed_mcp_servers_and_groups ||
- prevValues.mcp_tool_permissions !== currentValues.mcp_tool_permissions
- }
- >
- {() => (
-
- form.setFieldsValue({ mcp_tool_permissions: toolPerms })}
- />
-
- )}
-
-
-
-
-
-
- Logging Settings
-
-
-
-
-
-
-
-
- Model Aliases
-
-
-
-
- Create custom aliases for models that can be used by team members in API calls. This allows
- you to create shortcuts for specific models.
-
-
-
-
-
- >
-
- Create Team
-
-
-
- )}
-
-
-
- );
-};
-
-export default Teams;
diff --git a/ui/litellm-dashboard/tests/CreateKeyPage.expiredToken.test.tsx b/ui/litellm-dashboard/tests/CreateKeyPage.expiredToken.test.tsx
index 93e15312f63..e8d46355e00 100644
--- a/ui/litellm-dashboard/tests/CreateKeyPage.expiredToken.test.tsx
+++ b/ui/litellm-dashboard/tests/CreateKeyPage.expiredToken.test.tsx
@@ -18,10 +18,53 @@ const { stub, jwtDecodeMock } = vi.hoisted(() => {
* Mocks
* --------------------------- */
-// next/navigation: just return empty URLSearchParams (no invitation/page)
-vi.mock("next/navigation", () => ({
- useSearchParams: () => new URLSearchParams(""),
-}));
+vi.mock("@/hooks/useFeatureFlags", () => {
+ const React = require("react");
+
+ // minimal context so useFeatureFlags() returns something stable
+ const FeatureFlagsCtx = React.createContext({ get: () => false, flags: {} });
+
+ // Defensive provider: handle undefined props and allow optional value override
+ const FeatureFlagsProvider = (props: any) => {
+ const p = props || {};
+ const value = p.value ?? { get: () => false, flags: {} };
+ return React.createElement(FeatureFlagsCtx.Provider, { value }, p.children);
+ };
+
+ const useFeatureFlags = () => React.useContext(FeatureFlagsCtx);
+
+ return {
+ __esModule: true,
+ default: FeatureFlagsProvider, // supports default import
+ FeatureFlagsProvider, // supports named import
+ useFeatureFlags, // supports named import
+ };
+});
+
+// next/navigation mock: search params + router + pathname
+vi.mock("next/navigation", () => {
+ const router = {
+ push: vi.fn(),
+ replace: vi.fn(),
+ prefetch: vi.fn(),
+ back: vi.fn(),
+ forward: vi.fn(),
+ refresh: vi.fn(),
+ };
+
+ return {
+ __esModule: true,
+ // what your tests already relied on
+ useSearchParams: () => new URLSearchParams(""),
+ // added: satisfies useAuthorized / SidebarProvider
+ useRouter: () => router,
+ // optional helpers some components often read
+ usePathname: () => "/",
+ // optional: noop versions if code calls them
+ redirect: vi.fn(), // App Router server action usually; safe noop here
+ notFound: vi.fn(),
+ };
+});
// Networking layer
vi.mock("@/components/networking", () => {
@@ -87,6 +130,7 @@ vi.mock("@/lib/cva.config", () => ({
}));
import CreateKeyPage from "@/app/page";
+import { FeatureFlagsProvider } from "@/hooks/useFeatureFlags";
/** ----------------------------
* Helpers