feat(ui): cut the teams page over to the /ui/teams path route

OldTeams renders from app/(dashboard)/teams/page.tsx via useAuthorized. The
component already fetched its own paginated, filtered team list through
v2TeamListCall; setTeams was only a round-trip back into the shell's lifted
state, so it becomes internal useState. The organizations prop was redundant
with the useOrganizations hook the component already calls, and searchParams
was never read, so both are dropped. The shell keeps its own teams state and
fetch for the still-coupled api-keys and models arms.

Tests no longer inject teams through a prop; they mock teamListCall to drive
what the component renders.
This commit is contained in:
ryan-crabbe-berri 2026-06-12 21:35:37 -07:00
parent c90eb7e96f
commit f05d02aad2
7 changed files with 309 additions and 383 deletions

View file

@ -40,6 +40,7 @@ export const MIGRATED_E2E_PAGES: Record<string, string> = {
agents: "agents",
"router-settings": "router-settings",
users: "users",
teams: "teams",
};
export const MIGRATED_E2E_SEGMENTS: string[] = [...new Set(Object.values(MIGRATED_E2E_PAGES))];

View file

@ -6,7 +6,6 @@ import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings"
import LoadingScreen from "@/components/common_components/LoadingScreen";
import { Team } from "@/components/key_team_helpers/key_list";
import { Organization, proxyBaseUrl, getInProductNudgesCall } from "@/components/networking";
import OldTeams from "@/components/OldTeams";
import { fetchUserModels, CreateKeyPrefillData } from "@/components/organisms/create_key_button";
import Organizations, { fetchOrganizations } from "@/components/organizations";
import PassThroughSettings from "@/components/pass_through_settings";
@ -322,17 +321,6 @@ function CreateKeyPageContent() {
premiumUser={premiumUser}
teams={teams}
/>
) : page == "teams" ? (
<OldTeams
teams={teams}
setTeams={setTeams}
accessToken={accessToken}
userID={userID}
userRole={userRole}
organizations={organizations}
premiumUser={premiumUser}
searchParams={searchParams}
/>
) : page == "organizations" ? (
<Organizations
organizations={organizations}

View file

@ -0,0 +1,9 @@
"use client";
import OldTeams from "@/components/OldTeams";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
export default function TeamsPage() {
const { accessToken, userId, userRole, premiumUser } = useAuthorized();
return <OldTeams accessToken={accessToken} userID={userId} userRole={userRole} premiumUser={premiumUser ?? false} />;
}

View file

@ -5,6 +5,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { fetchAvailableModelsForTeamOrKey } from "./key_team_helpers/fetch_available_models_team_key";
import { fetchMCPAccessGroups, getGuardrailsList, teamCreateCall } from "./networking";
import OldTeams from "./OldTeams";
import { teamListCall } from "@/app/(dashboard)/hooks/teams/useTeams";
const mockTeamInfoView = vi.fn();
const mockUseOrganizations = vi.fn();
@ -349,32 +350,29 @@ describe("OldTeams - handleCreate organization handling", () => {
it("should clear the delete modal when the cancel button is clicked", async () => {
mockUseOrganizations.mockReturnValue({ data: [] });
renderWithQueryClient(
<OldTeams
teams={[
{
team_id: "1",
team_alias: "Test Team",
organization_id: "org-123",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [],
members_with_roles: [],
spend: 0,
},
]}
searchParams={{}}
accessToken="test-token"
setTeams={vi.fn()}
userID="user-123"
userRole="Admin"
organizations={[]}
/>,
);
vi.mocked(teamListCall).mockResolvedValue({
teams: [
{
team_id: "1",
team_alias: "Test Team",
organization_id: "org-123",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [],
members_with_roles: [],
spend: 0,
},
],
total: 1,
page: 1,
page_size: 100,
total_pages: 1,
} as any);
renderWithQueryClient(<OldTeams accessToken="test-token" userID="user-123" userRole="Admin" />);
await waitFor(() => {
expect(screen.getByTestId("delete-team-button")).toBeInTheDocument();
});
@ -393,17 +391,8 @@ describe("OldTeams - empty state", () => {
});
it("should display empty state message when teams array is empty", async () => {
renderWithQueryClient(
<OldTeams
teams={[]}
searchParams={{}}
accessToken="test-token"
setTeams={vi.fn()}
userID="user-123"
userRole="Admin"
organizations={[]}
/>,
);
vi.mocked(teamListCall).mockResolvedValue({ teams: [], total: 0, page: 1, page_size: 100, total_pages: 1 } as any);
renderWithQueryClient(<OldTeams accessToken="test-token" userID="user-123" userRole="Admin" />);
await waitFor(() => {
expect(screen.getByText("No teams yet")).toBeInTheDocument();
@ -414,17 +403,8 @@ describe("OldTeams - empty state", () => {
});
it("should display empty state message when teams is null", async () => {
renderWithQueryClient(
<OldTeams
teams={null}
searchParams={{}}
accessToken="test-token"
setTeams={vi.fn()}
userID="user-123"
userRole="Admin"
organizations={[]}
/>,
);
vi.mocked(teamListCall).mockResolvedValue({ teams: [], total: 0, page: 1, page_size: 100, total_pages: 1 } as any);
renderWithQueryClient(<OldTeams accessToken="test-token" userID="user-123" userRole="Admin" />);
await waitFor(() => {
expect(screen.getByText("No teams yet")).toBeInTheDocument();
@ -435,32 +415,29 @@ describe("OldTeams - empty state", () => {
});
it("should not display empty state when teams array has items", async () => {
renderWithQueryClient(
<OldTeams
teams={[
{
team_id: "1",
team_alias: "Test Team",
organization_id: "org-123",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [],
members_with_roles: [],
spend: 0,
},
]}
searchParams={{}}
accessToken="test-token"
setTeams={vi.fn()}
userID="user-123"
userRole="Admin"
organizations={[]}
/>,
);
vi.mocked(teamListCall).mockResolvedValue({
teams: [
{
team_id: "1",
team_alias: "Test Team",
organization_id: "org-123",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [],
members_with_roles: [],
spend: 0,
},
],
total: 1,
page: 1,
page_size: 100,
total_pages: 1,
} as any);
renderWithQueryClient(<OldTeams accessToken="test-token" userID="user-123" userRole="Admin" />);
await waitFor(() => {
expect(screen.getByText("Test Team")).toBeInTheDocument();
@ -608,33 +585,29 @@ describe("OldTeams - premium props", () => {
});
it("passes premiumUser flag to TeamInfoView", async () => {
renderWithQueryClient(
<OldTeams
teams={[
{
team_id: "team-123456789",
team_alias: "Premium Team",
organization_id: "org-123",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [],
members_with_roles: [],
spend: 0,
},
]}
searchParams={{}}
accessToken="test-token"
setTeams={vi.fn()}
userID="user-123"
userRole="Admin"
organizations={[]}
premiumUser={true}
/>,
);
vi.mocked(teamListCall).mockResolvedValue({
teams: [
{
team_id: "team-123456789",
team_alias: "Premium Team",
organization_id: "org-123",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [],
members_with_roles: [],
spend: 0,
},
],
total: 1,
page: 1,
page_size: 100,
total_pages: 1,
} as any);
renderWithQueryClient(<OldTeams accessToken="test-token" userID="user-123" userRole="Admin" premiumUser={true} />);
const teamIdElement = await screen.findByText("team-123456789");
act(() => {
@ -654,125 +627,113 @@ describe("OldTeams - Default Team Settings tab visibility", () => {
});
it("should show Default Team Settings tab for Admin role", () => {
renderWithQueryClient(
<OldTeams
teams={[
{
team_id: "1",
team_alias: "Test Team",
organization_id: "org-123",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [],
members_with_roles: [],
spend: 0,
},
]}
searchParams={{}}
accessToken="test-token"
setTeams={vi.fn()}
userID="user-123"
userRole="Admin"
organizations={[]}
/>,
);
vi.mocked(teamListCall).mockResolvedValue({
teams: [
{
team_id: "1",
team_alias: "Test Team",
organization_id: "org-123",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [],
members_with_roles: [],
spend: 0,
},
],
total: 1,
page: 1,
page_size: 100,
total_pages: 1,
} as any);
renderWithQueryClient(<OldTeams accessToken="test-token" userID="user-123" userRole="Admin" />);
expect(screen.getByRole("tab", { name: "Default Team Settings" })).toBeInTheDocument();
});
it("should show Default Team Settings tab for proxy_admin role", () => {
renderWithQueryClient(
<OldTeams
teams={[
{
team_id: "1",
team_alias: "Test Team",
organization_id: "org-123",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [],
members_with_roles: [],
spend: 0,
},
]}
searchParams={{}}
accessToken="test-token"
setTeams={vi.fn()}
userID="user-123"
userRole="proxy_admin"
organizations={[]}
/>,
);
vi.mocked(teamListCall).mockResolvedValue({
teams: [
{
team_id: "1",
team_alias: "Test Team",
organization_id: "org-123",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [],
members_with_roles: [],
spend: 0,
},
],
total: 1,
page: 1,
page_size: 100,
total_pages: 1,
} as any);
renderWithQueryClient(<OldTeams accessToken="test-token" userID="user-123" userRole="proxy_admin" />);
expect(screen.getByRole("tab", { name: "Default Team Settings" })).toBeInTheDocument();
});
it("should not show Default Team Settings tab for proxy_admin_viewer role", () => {
renderWithQueryClient(
<OldTeams
teams={[
{
team_id: "1",
team_alias: "Test Team",
organization_id: "org-123",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [],
members_with_roles: [],
spend: 0,
},
]}
searchParams={{}}
accessToken="test-token"
setTeams={vi.fn()}
userID="user-123"
userRole="proxy_admin_viewer"
organizations={[]}
/>,
);
vi.mocked(teamListCall).mockResolvedValue({
teams: [
{
team_id: "1",
team_alias: "Test Team",
organization_id: "org-123",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [],
members_with_roles: [],
spend: 0,
},
],
total: 1,
page: 1,
page_size: 100,
total_pages: 1,
} as any);
renderWithQueryClient(<OldTeams accessToken="test-token" userID="user-123" userRole="proxy_admin_viewer" />);
expect(screen.queryByRole("tab", { name: "Default Team Settings" })).not.toBeInTheDocument();
});
it("should not show Default Team Settings tab for Admin Viewer role", () => {
renderWithQueryClient(
<OldTeams
teams={[
{
team_id: "1",
team_alias: "Test Team",
organization_id: "org-123",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [],
members_with_roles: [],
spend: 0,
},
]}
searchParams={{}}
accessToken="test-token"
setTeams={vi.fn()}
userID="user-123"
userRole="Admin Viewer"
organizations={[]}
/>,
);
vi.mocked(teamListCall).mockResolvedValue({
teams: [
{
team_id: "1",
team_alias: "Test Team",
organization_id: "org-123",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [],
members_with_roles: [],
spend: 0,
},
],
total: 1,
page: 1,
page_size: 100,
total_pages: 1,
} as any);
renderWithQueryClient(<OldTeams accessToken="test-token" userID="user-123" userRole="Admin Viewer" />);
expect(screen.queryByRole("tab", { name: "Default Team Settings" })).not.toBeInTheDocument();
});
@ -800,17 +761,8 @@ describe("OldTeams - access_group_ids in team create", () => {
});
it("should pass access_group_ids to teamCreateCall when creating team", async () => {
renderWithQueryClient(
<OldTeams
teams={[]}
searchParams={{}}
accessToken="test-token"
setTeams={vi.fn()}
userID="user-123"
userRole="Admin"
organizations={[{ organization_id: "org-1", organization_alias: "Org 1", models: [], members: [] }]}
/>,
);
vi.mocked(teamListCall).mockResolvedValue({ teams: [], total: 0, page: 1, page_size: 100, total_pages: 1 } as any);
renderWithQueryClient(<OldTeams accessToken="test-token" userID="user-123" userRole="Admin" />);
const createButton = screen.getAllByRole("button", { name: /create team/i })[0];
act(() => {
@ -864,17 +816,8 @@ describe("OldTeams - models dropdown options", () => {
it("should not render all-proxy-models option in models select", async () => {
vi.mocked(fetchAvailableModelsForTeamOrKey).mockResolvedValue(["gpt-4", "gpt-3.5-turbo"]);
renderWithQueryClient(
<OldTeams
teams={[]}
searchParams={{}}
accessToken="test-token"
setTeams={vi.fn()}
userID="user-123"
userRole="Admin"
organizations={[]}
/>,
);
vi.mocked(teamListCall).mockResolvedValue({ teams: [], total: 0, page: 1, page_size: 100, total_pages: 1 } as any);
renderWithQueryClient(<OldTeams accessToken="test-token" userID="user-123" userRole="Admin" />);
await waitFor(() => {
expect(fetchAvailableModelsForTeamOrKey).toHaveBeenCalled();
@ -922,32 +865,29 @@ describe("OldTeams - organization alias display", () => {
mockUseOrganizations.mockReturnValue({ data: mockOrganizations });
renderWithQueryClient(
<OldTeams
teams={[
{
team_id: "1",
team_alias: "Test Team",
organization_id: "org-123",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [],
members_with_roles: [],
spend: 0,
},
]}
searchParams={{}}
accessToken="test-token"
setTeams={vi.fn()}
userID="user-123"
userRole="Admin"
organizations={mockOrganizations}
/>,
);
vi.mocked(teamListCall).mockResolvedValue({
teams: [
{
team_id: "1",
team_alias: "Test Team",
organization_id: "org-123",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [],
members_with_roles: [],
spend: 0,
},
],
total: 1,
page: 1,
page_size: 100,
total_pages: 1,
} as any);
renderWithQueryClient(<OldTeams accessToken="test-token" userID="user-123" userRole="Admin" />);
await waitFor(() => {
expect(screen.getByText("Test Organization")).toBeInTheDocument();
@ -958,32 +898,29 @@ describe("OldTeams - organization alias display", () => {
it("should display organization id when alias is not found", async () => {
mockUseOrganizations.mockReturnValue({ data: [] });
renderWithQueryClient(
<OldTeams
teams={[
{
team_id: "1",
team_alias: "Test Team",
organization_id: "org-unknown",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [],
members_with_roles: [],
spend: 0,
},
]}
searchParams={{}}
accessToken="test-token"
setTeams={vi.fn()}
userID="user-123"
userRole="Admin"
organizations={[]}
/>,
);
vi.mocked(teamListCall).mockResolvedValue({
teams: [
{
team_id: "1",
team_alias: "Test Team",
organization_id: "org-unknown",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [],
members_with_roles: [],
spend: 0,
},
],
total: 1,
page: 1,
page_size: 100,
total_pages: 1,
} as any);
renderWithQueryClient(<OldTeams accessToken="test-token" userID="user-123" userRole="Admin" />);
await waitFor(() => {
expect(screen.getByText("org-unknown")).toBeInTheDocument();
@ -993,32 +930,29 @@ describe("OldTeams - organization alias display", () => {
it("should display N/A when organization_id is null", async () => {
mockUseOrganizations.mockReturnValue({ data: [] });
renderWithQueryClient(
<OldTeams
teams={[
{
team_id: "1",
team_alias: "Test Team",
organization_id: null as any,
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [],
members_with_roles: [],
spend: 0,
},
]}
searchParams={{}}
accessToken="test-token"
setTeams={vi.fn()}
userID="user-123"
userRole="Admin"
organizations={[]}
/>,
);
vi.mocked(teamListCall).mockResolvedValue({
teams: [
{
team_id: "1",
team_alias: "Test Team",
organization_id: null as any,
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [],
members_with_roles: [],
spend: 0,
},
],
total: 1,
page: 1,
page_size: 100,
total_pages: 1,
} as any);
renderWithQueryClient(<OldTeams accessToken="test-token" userID="user-123" userRole="Admin" />);
await waitFor(() => {
// When organization_id is null, the table shows "—" in the Organization column
@ -1034,32 +968,31 @@ describe("OldTeams - Resources column keys badge", () => {
});
it("renders keys_count from the v2 payload in the Resources badge", async () => {
vi.mocked(teamListCall).mockResolvedValue({
teams: [
{
team_id: "1",
team_alias: "Team With Keys",
organization_id: "org-123",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [],
keys_count: 3,
members_with_roles: [],
spend: 0,
} as any,
],
total: 1,
page: 1,
page_size: 100,
total_pages: 1,
} as any);
const { container } = renderWithQueryClient(
<OldTeams
teams={[
{
team_id: "1",
team_alias: "Team With Keys",
organization_id: "org-123",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [],
keys_count: 3,
members_with_roles: [],
spend: 0,
} as any,
]}
searchParams={{}}
accessToken="test-token"
setTeams={vi.fn()}
userID="user-123"
userRole="Admin"
organizations={[]}
/>,
<OldTeams accessToken="test-token" userID="user-123" userRole="Admin" />,
);
await waitFor(() => {
@ -1071,31 +1004,30 @@ describe("OldTeams - Resources column keys badge", () => {
});
it("falls back to keys.length when keys_count is absent", async () => {
vi.mocked(teamListCall).mockResolvedValue({
teams: [
{
team_id: "2",
team_alias: "Legacy Team",
organization_id: "org-123",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [{ token: "t1" } as any, { token: "t2" } as any],
members_with_roles: [],
spend: 0,
} as any,
],
total: 1,
page: 1,
page_size: 100,
total_pages: 1,
} as any);
const { container } = renderWithQueryClient(
<OldTeams
teams={[
{
team_id: "2",
team_alias: "Legacy Team",
organization_id: "org-123",
models: ["gpt-4"],
max_budget: 100,
budget_duration: "1d",
tpm_limit: 1000,
rpm_limit: 1000,
created_at: new Date().toISOString(),
keys: [{ token: "t1" } as any, { token: "t2" } as any],
members_with_roles: [],
spend: 0,
} as any,
]}
searchParams={{}}
accessToken="test-token"
setTeams={vi.fn()}
userID="user-123"
userRole="Admin"
organizations={[]}
/>,
<OldTeams accessToken="test-token" userID="user-123" userRole="Admin" />,
);
await waitFor(() => {

View file

@ -54,13 +54,9 @@ import VectorStoreSelector from "./vector_store_management/VectorStoreSelector";
import SearchToolSelector from "./SearchTools/SearchToolSelector";
interface TeamProps {
teams: Team[] | null;
searchParams: any;
accessToken: string | null;
setTeams: React.Dispatch<React.SetStateAction<Team[] | null>>;
userID: string | null;
userRole: string | null;
organizations: Organization[] | null;
premiumUser?: boolean;
}
@ -165,18 +161,10 @@ const getOrganizationAlias = (
};
// @deprecated
const Teams: React.FC<TeamProps> = ({
teams,
searchParams,
accessToken,
setTeams,
userID,
userRole,
organizations,
premiumUser = false,
}) => {
console.log(`organizations: ${JSON.stringify(organizations)}`);
const Teams: React.FC<TeamProps> = ({ accessToken, userID, userRole, premiumUser = false }) => {
const { data: organizationsData } = useOrganizations();
const organizations = organizationsData ?? null;
const [teams, setTeams] = useState<Team[] | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [fetchError, setFetchError] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(1);
@ -721,7 +709,7 @@ const Teams: React.FC<TeamProps> = ({
width: 160,
ellipsis: true,
render: (_: unknown, record: Team) => {
const orgAlias = getOrganizationAlias(record.organization_id, organizationsData || organizations);
const orgAlias = getOrganizationAlias(record.organization_id, organizations);
return record.organization_id ? (
<Text ellipsis style={{ fontSize: 14 }}>
{orgAlias}
@ -860,7 +848,7 @@ const Teams: React.FC<TeamProps> = ({
),
},
],
[userRole, perTeamInfo, organizationsData, organizations],
[userRole, perTeamInfo, organizations],
);
const displayTeams = useMemo(() => teams ?? [], [teams]);

View file

@ -126,6 +126,13 @@ describe("migratedHref / legacyPageHref", () => {
expect(MIGRATED_PAGES.users).toBe("users");
});
it("maps the teams id to its route", async () => {
vi.doMock("@/components/networking", () => ({ serverRootPath: "/" }));
const { MIGRATED_PAGES } = await import("./migratedPages");
expect(MIGRATED_PAGES.teams).toBe("teams");
});
});
describe("dev server (NODE_ENV=development)", () => {

View file

@ -43,6 +43,7 @@ export const MIGRATED_PAGES: Record<string, string> = {
agents: "agents",
"router-settings": "router-settings",
users: "users",
teams: "teams",
};
function uiBase(): string {