mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
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
This commit is contained in:
parent
b3b2ec5d58
commit
a9b8b5f2fd
4 changed files with 117 additions and 75 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<string[]>([]);
|
||||
|
||||
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 <LoadingScreen />;
|
||||
}
|
||||
if (!teamId || isPending) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
const team = teams?.find((t) => t.team_id === teamId);
|
||||
if (!team) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<Button type="text" icon={<ArrowLeftOutlined />} onClick={backToTeams} className="mb-4">
|
||||
Back to Teams
|
||||
</Button>
|
||||
<p className="text-sm text-gray-700">Team not found</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isTeamAdmin = (team.members_with_roles ?? []).some((m) => m.user_id === userId && m.role === "admin");
|
||||
|
||||
return (
|
||||
<TeamInfoView
|
||||
teamId={teamId}
|
||||
accessToken={accessToken}
|
||||
is_team_admin={isTeamAdmin}
|
||||
is_proxy_admin={userRole === "Admin"}
|
||||
userModels={userModels}
|
||||
editTeam={false}
|
||||
premiumUser={premiumUser ?? undefined}
|
||||
onClose={backToTeams}
|
||||
onUpdate={() => queryClient.invalidateQueries({ queryKey: ["teams"] })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import TeamDetailPage from "./TeamDetailPage";
|
||||
|
||||
export function generateStaticParams() {
|
||||
return [{ teamid: "placeholder" }];
|
||||
}
|
||||
|
||||
export default function TeamDetailRoute() {
|
||||
return <TeamDetailPage />;
|
||||
}
|
||||
|
|
@ -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<TeamProps> = ({ accessToken, userID, userRole, premiumUser
|
|||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
|
||||
const [selectedTeam, setSelectedTeam] = useState<null | any>(null);
|
||||
const [selectedTeamId, setSelectedTeamId] = useState<string | null>(null);
|
||||
const [editTeam, setEditTeam] = useState<boolean>(false);
|
||||
const router = useRouter();
|
||||
|
||||
const [isTeamModalVisible, setIsTeamModalVisible] = useState(false);
|
||||
const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false);
|
||||
|
|
@ -582,19 +581,6 @@ const Teams: React.FC<TeamProps> = ({ 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<TeamProps> = ({ accessToken, userID, userRole, premiumUser
|
|||
width: 170,
|
||||
ellipsis: true,
|
||||
render: (id: string) => (
|
||||
<IdCell value={id} onClick={(teamId) => setSelectedTeamId(teamId)} dataTestId="team-id-cell" />
|
||||
<IdCell
|
||||
value={id}
|
||||
onClick={(teamId) => router.push(migratedHref(`teams/${encodeURIComponent(teamId)}/`))}
|
||||
dataTestId="team-id-cell"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
|
@ -813,10 +803,7 @@ const Teams: React.FC<TeamProps> = ({ 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)}/`))}
|
||||
/>
|
||||
<TableIconActionButton
|
||||
variant="Delete"
|
||||
|
|
@ -988,59 +975,27 @@ const Teams: React.FC<TeamProps> = ({ accessToken, userID, userRole, premiumUser
|
|||
|
||||
return (
|
||||
<Content style={{ padding: token.paddingLG, paddingInline: token.paddingLG * 2 }}>
|
||||
{selectedTeamId ? (
|
||||
<TeamInfoView
|
||||
teamId={selectedTeamId}
|
||||
onUpdate={(data) => {
|
||||
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}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Flex justify="space-between" align="center" style={{ marginBottom: 16 }}>
|
||||
<Space direction="vertical" size={0}>
|
||||
<Title level={2} style={{ margin: 0 }}>
|
||||
<TeamOutlined style={{ marginRight: 8 }} />
|
||||
Teams
|
||||
</Title>
|
||||
<Text type="secondary">Manage teams, members, and their access to models and budgets</Text>
|
||||
</Space>
|
||||
{canCreateOrManageTeams(userRole, userID, organizations) && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setIsTeamModalVisible(true)}
|
||||
data-testid="create-team-button"
|
||||
>
|
||||
Create Team
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center" style={{ marginBottom: 16 }}>
|
||||
<Space direction="vertical" size={0}>
|
||||
<Title level={2} style={{ margin: 0 }}>
|
||||
<TeamOutlined style={{ marginRight: 8 }} />
|
||||
Teams
|
||||
</Title>
|
||||
<Text type="secondary">Manage teams, members, and their access to models and budgets</Text>
|
||||
</Space>
|
||||
{canCreateOrManageTeams(userRole, userID, organizations) && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setIsTeamModalVisible(true)}
|
||||
data-testid="create-team-button"
|
||||
>
|
||||
Create Team
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Tabs items={tabItems} />
|
||||
</>
|
||||
)}
|
||||
<Tabs items={tabItems} />
|
||||
|
||||
{canCreateOrManageTeams(userRole, userID, organizations) && (
|
||||
<Modal
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue