From a9b8b5f2fddc9939aafefce9164623637b323d20 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Thu, 9 Jul 2026 23:15:04 -0700 Subject: [PATCH] feat(ui): add /teams/[teamid] team detail route Mirrors the api-keys detail route for teams. Clicking a team's Team ID (and the row Edit action) now navigates to /teams/[teamid] instead of swapping an inline TeamInfoView, so each team gets a real, linkable URL. TeamDetailPage reads the id from window.location, resolves the team from the teams list (for the admin check and a not-found guard), fetches available models, and renders the existing TeamInfoView; back button returns to /teams. The inline swap and its now-unused selectedTeamId/editTeam state and is_team_admin helper are removed. No backend change: the SPAStaticFiles fallback from the base PR already serves the /ui/teams/* shell --- ui/litellm-dashboard/eslint-metrics.json | 2 +- .../teams/[teamid]/TeamDetailPage.tsx | 78 +++++++++++++ .../app/(dashboard)/teams/[teamid]/page.tsx | 9 ++ .../src/components/OldTeams.tsx | 103 +++++------------- 4 files changed, 117 insertions(+), 75 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/teams/[teamid]/TeamDetailPage.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/teams/[teamid]/page.tsx diff --git a/ui/litellm-dashboard/eslint-metrics.json b/ui/litellm-dashboard/eslint-metrics.json index d6c88f704fa..61418cc1e55 100644 --- a/ui/litellm-dashboard/eslint-metrics.json +++ b/ui/litellm-dashboard/eslint-metrics.json @@ -1,5 +1,5 @@ { - "@typescript-eslint/no-explicit-any": 1978, + "@typescript-eslint/no-explicit-any": 1977, "complexity": 129, "local/no-large-inline-object-arg": 513, "local/no-long-condition-chain": 233, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/[teamid]/TeamDetailPage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/[teamid]/TeamDetailPage.tsx new file mode 100644 index 00000000000..1655c1182e4 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/[teamid]/TeamDetailPage.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { useAllTeams } from "@/app/(dashboard)/hooks/teams/useTeams"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import LoadingScreen from "@/components/common_components/LoadingScreen"; +import { fetchAvailableModelsForTeamOrKey } from "@/components/key_team_helpers/fetch_available_models_team_key"; +import TeamInfoView from "@/components/team/TeamInfo"; +import { migratedHref } from "@/utils/migratedPages"; +import { ArrowLeftOutlined } from "@ant-design/icons"; +import { useQueryClient } from "@tanstack/react-query"; +import { Button } from "antd"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; + +function teamIdFromPathname(pathname: string): string { + const segments = pathname.replace(/\/+$/, "").split("/"); + return decodeURIComponent(segments[segments.length - 1] ?? ""); +} + +export default function TeamDetailPage() { + const router = useRouter(); + const queryClient = useQueryClient(); + const { isLoading: authLoading, isAuthorized, accessToken, userId, userRole, premiumUser } = useAuthorized(); + const [teamId] = useState(() => (typeof window === "undefined" ? "" : teamIdFromPathname(window.location.pathname))); + const [userModels, setUserModels] = useState([]); + + const { data: teams, isPending } = useAllTeams(); + + useEffect(() => { + if (!accessToken || !userId || !userRole) { + return; + } + fetchAvailableModelsForTeamOrKey(userId, userRole, accessToken) + .then((models) => { + if (models) { + setUserModels(models); + } + }) + .catch((error) => console.error("Error fetching user models:", error)); + }, [accessToken, userId, userRole]); + + const backToTeams = () => router.push(migratedHref("teams")); + + if (authLoading || !isAuthorized) { + return ; + } + if (!teamId || isPending) { + return ; + } + + const team = teams?.find((t) => t.team_id === teamId); + if (!team) { + return ( +
+ +

Team not found

+
+ ); + } + + const isTeamAdmin = (team.members_with_roles ?? []).some((m) => m.user_id === userId && m.role === "admin"); + + return ( + queryClient.invalidateQueries({ queryKey: ["teams"] })} + /> + ); +} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/teams/[teamid]/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/teams/[teamid]/page.tsx new file mode 100644 index 00000000000..744c00ab8de --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/teams/[teamid]/page.tsx @@ -0,0 +1,9 @@ +import TeamDetailPage from "./TeamDetailPage"; + +export function generateStaticParams() { + return [{ teamid: "placeholder" }]; +} + +export default function TeamDetailRoute() { + return ; +} diff --git a/ui/litellm-dashboard/src/components/OldTeams.tsx b/ui/litellm-dashboard/src/components/OldTeams.tsx index e83d1acf4e5..5192556b247 100644 --- a/ui/litellm-dashboard/src/components/OldTeams.tsx +++ b/ui/litellm-dashboard/src/components/OldTeams.tsx @@ -1,6 +1,7 @@ import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations"; import AvailableTeamsPanel from "@/components/team/available_teams"; -import TeamInfoView from "@/components/team/TeamInfo"; +import { migratedHref } from "@/utils/migratedPages"; +import { useRouter } from "next/navigation"; import TeamSSOSettings from "@/components/TeamSSOSettings"; import { isProxyAdminRole } from "@/utils/roles"; import { InfoCircleOutlined, PlusOutlined, TeamOutlined, ReloadOutlined } from "@ant-design/icons"; @@ -75,7 +76,6 @@ interface EditTeamModalProps { onSubmit: (data: FormData) => void; // Assuming FormData is the type of data to be submitted } -import { updateExistingKeys } from "@/utils/dataUtils"; import DeleteResourceModal from "./common_components/DeleteResourceModal"; import { Member, teamCreateCall } from "./networking"; import { ModelSelect } from "./ModelSelect/ModelSelect"; @@ -228,8 +228,7 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser const [editModalVisible, setEditModalVisible] = useState(false); const [selectedTeam, setSelectedTeam] = useState(null); - const [selectedTeamId, setSelectedTeamId] = useState(null); - const [editTeam, setEditTeam] = useState(false); + const router = useRouter(); const [isTeamModalVisible, setIsTeamModalVisible] = useState(false); const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false); @@ -582,19 +581,6 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser } }; - 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 handleSearchChange = (value: string) => { if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current); setIsSearching(true); @@ -672,7 +658,11 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser width: 170, ellipsis: true, render: (id: string) => ( - setSelectedTeamId(teamId)} dataTestId="team-id-cell" /> + router.push(migratedHref(`teams/${encodeURIComponent(teamId)}/`))} + dataTestId="team-id-cell" + /> ), }, { @@ -813,10 +803,7 @@ const Teams: React.FC = ({ accessToken, userID, userRole, premiumUser variant="Edit" tooltipText="Edit team" dataTestId="edit-team-button" - onClick={() => { - setSelectedTeamId(record.team_id); - setEditTeam(true); - }} + onClick={() => router.push(migratedHref(`teams/${encodeURIComponent(record.team_id)}/`))} /> = ({ accessToken, userID, userRole, premiumUser return ( - {selectedTeamId ? ( - { - setTeams((teams) => { - if (teams == null) { - return teams; - } - return teams.map((team) => { - if (data.team_id === team.team_id) { - return updateExistingKeys(team, data); - } - return team; - }); - }); - fetchTeamsV2(); - }} - 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} - premiumUser={premiumUser} - /> - ) : ( - <> - - - - <TeamOutlined style={{ marginRight: 8 }} /> - Teams - - Manage teams, members, and their access to models and budgets - - {canCreateOrManageTeams(userRole, userID, organizations) && ( - - )} - + + + + <TeamOutlined style={{ marginRight: 8 }} /> + Teams + + Manage teams, members, and their access to models and budgets + + {canCreateOrManageTeams(userRole, userID, organizations) && ( + + )} + - - - )} + {canCreateOrManageTeams(userRole, userID, organizations) && (