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:
ryan-crabbe-berri 2026-07-09 23:15:04 -07:00
parent b3b2ec5d58
commit a9b8b5f2fd
4 changed files with 117 additions and 75 deletions

View file

@ -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,

View file

@ -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"] })}
/>
);
}

View file

@ -0,0 +1,9 @@
import TeamDetailPage from "./TeamDetailPage";
export function generateStaticParams() {
return [{ teamid: "placeholder" }];
}
export default function TeamDetailRoute() {
return <TeamDetailPage />;
}

View file

@ -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