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/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/page.tsx index 42e6d250ee4..00f482a6faf 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 Teams from "@/app/(dashboard)/teams/teams"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import useTeams from "@/app/(dashboard)/hooks/useTeams"; import { useEffect, useState } from "react"; diff --git a/ui/litellm-dashboard/src/components/teams.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/teams.tsx similarity index 56% rename from ui/litellm-dashboard/src/components/teams.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/teams/teams.tsx index 590e0952944..b095ef1cae4 100644 --- a/ui/litellm-dashboard/src/components/teams.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/teams.tsx @@ -1,37 +1,19 @@ import React, { useState, useEffect } from "react"; -import Link from "next/link"; import { Typography } from "antd"; -import { - teamDeleteCall, - teamUpdateCall, - teamInfoCall, - Organization, - DEFAULT_ORGANIZATION, - fetchMCPAccessGroups, -} from "./networking"; -import TeamMemberModal from "@/components/team/edit_membership"; -import { fetchTeams } from "./common_components/fetch_teams"; -import { - InformationCircleIcon, - PencilAltIcon, - PencilIcon, - RefreshIcon, - StatusOnlineIcon, - TrashIcon, - ChevronDownIcon, - ChevronRightIcon, -} from "@heroicons/react/outline"; +import { teamDeleteCall, Organization, fetchMCPAccessGroups } from "@/components/networking"; +import { fetchTeams } from "@/components/common_components/fetch_teams"; +import { PencilAltIcon, RefreshIcon, TrashIcon, ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline"; import { Button as Button2, Modal, Form, Input, Select as Select2, message, Tooltip } from "antd"; -import NumericalInput from "./shared/numerical_input"; +import NumericalInput from "../../../components/shared/numerical_input"; import { fetchAvailableModelsForTeamOrKey, getModelDisplayName, unfurlWildcardModelsInList, -} from "./key_team_helpers/fetch_available_models_team_key"; +} from "@/components/key_team_helpers/fetch_available_models_team_key"; import { Select, SelectItem } from "@tremor/react"; import { InfoCircleOutlined } from "@ant-design/icons"; -import { getGuardrailsList } from "./networking"; -import TeamInfoView, { TeamData } from "@/components/team/team_info"; +import { getGuardrailsList } from "@/components/networking"; +import TeamInfoView from "@/components/team/team_info"; import TeamSSOSettings from "@/components/TeamSSOSettings"; import { isAdminRole } from "@/utils/roles"; import { @@ -58,12 +40,11 @@ import { TabPanels, Tab, } from "@tremor/react"; -import { CogIcon } from "@heroicons/react/outline"; import AvailableTeamsPanel from "@/components/team/available_teams"; -import VectorStoreSelector from "./vector_store_management/VectorStoreSelector"; -import PremiumLoggingSettings from "./common_components/PremiumLoggingSettings"; -import type { KeyResponse, Team } from "./key_team_helpers/key_list"; -import { formatNumberWithCommas } from "../utils/dataUtils"; +import VectorStoreSelector from "../../../components/vector_store_management/VectorStoreSelector"; +import PremiumLoggingSettings from "../../../components/common_components/PremiumLoggingSettings"; +import type { KeyResponse, Team } from "@/components/key_team_helpers/key_list"; +import { formatNumberWithCommas } from "@/utils/dataUtils"; import { AlertTriangleIcon, XIcon } from "lucide-react"; import MCPServerSelector from "./mcp_server_management/MCPServerSelector"; import MCPToolPermissions from "./mcp_server_management/MCPToolPermissions"; @@ -96,15 +77,10 @@ interface EditTeamModalProps { onSubmit: (data: FormData) => void; // Assuming FormData is the type of data to be submitted } -import { - teamCreateCall, - teamMemberAddCall, - teamMemberUpdateCall, - Member, - modelAvailableCall, - v2TeamListCall, -} from "./networking"; +import { teamCreateCall, 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"; interface TeamInfo { members_with_roles: Member[]; @@ -167,11 +143,7 @@ const Teams: React.FC = ({ 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); @@ -573,450 +545,328 @@ const Teams: React.FC = ({ 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 */} -
- -
-
- )} -
+ + + + Click on “Team ID” to view team details and manage team members. + + + + +
+
+
- - - - Team Name - Team ID - Created - Spend (USD) - Budget (USD) - Models - Organization - Info - - + +
+ + + 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] && ( - + + {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 + + ) : ( + - +{team.models.length - 3}{" "} - {team.models.length - 3 === 1 - ? "more model" - : "more models"} + {model.length > 30 + ? `${getModelDisplayName(model).slice(0, 30)}...` + : getModelDisplayName(model)} - )} - {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)} - - - ), - )} -
- )} -
+ ), + )} + {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 - />
-
-
-
+ + + {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 || "") && ( - + - {isAdminRole(userRole || "") && ( - - - - )} - - + )} + )} {(userRole == "Admin" || userRole == "Org Admin") && ( = ({ - + shouldUpdate={(prevValues, currentValues) => prevValues.allowed_mcp_servers_and_groups !== currentValues.allowed_mcp_servers_and_groups || prevValues.mcp_tool_permissions !== currentValues.mcp_tool_permissions } diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index cefa680fc82..4b5e19b60f9 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -10,7 +10,7 @@ import { ThemeProvider } from "@/contexts/ThemeContext"; import UserDashboard from "@/components/user_dashboard"; import ModelDashboard from "@/components/templates/model_dashboard"; import ViewUserDashboard from "@/components/view_users"; -import Teams from "@/components/teams"; +import Teams from "@/app/(dashboard)/teams/teams"; import Organizations from "@/components/organizations"; import { fetchOrganizations } from "@/components/organizations"; import AdminPanel from "@/components/admins";