mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
refactor(ui): migrate available teams table onto shared DataTable
This commit is contained in:
parent
9ad8698aab
commit
99f215df68
8 changed files with 365 additions and 282 deletions
|
|
@ -2147,11 +2147,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/team/available_teams.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/team/member_permissions.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
|
||||
import AvailableTeamsPanel from "@/components/team/available_teams";
|
||||
import AvailableTeamsPanel from "@/components/team/AvailableTeamsPanel";
|
||||
import TeamInfoView from "@/components/team/TeamInfo";
|
||||
import TeamSSOSettings from "@/components/TeamSSOSettings";
|
||||
import { isProxyAdminRole } from "@/utils/roles";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
import * as networking from "@/components/networking";
|
||||
import { act, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "../../../tests/test-utils";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import AvailableTeamsPanel from "./AvailableTeamsPanel";
|
||||
import type { AvailableTeam } from "./AvailableTeamsTableColumns";
|
||||
|
||||
vi.mock("@/components/networking", () => ({
|
||||
availableTeamListCall: vi.fn(),
|
||||
teamMemberAddCall: vi.fn(),
|
||||
}));
|
||||
|
||||
const team = (overrides: Partial<AvailableTeam> = {}): AvailableTeam => ({
|
||||
team_id: "team-1",
|
||||
team_alias: "Test Team 1",
|
||||
description: "Test Description 1",
|
||||
models: ["gpt-4"],
|
||||
members_with_roles: [{ user_id: "user-1", user_email: "user1@test.com", role: "admin" }],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("AvailableTeamsPanel", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render the column headers", async () => {
|
||||
vi.mocked(networking.availableTeamListCall).mockResolvedValue([team()]);
|
||||
|
||||
renderWithProviders(<AvailableTeamsPanel accessToken="token-123" userID="user-123" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Team Name")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("Models")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display teams when available", async () => {
|
||||
const mockTeams = [
|
||||
team({ team_id: "team-1", team_alias: "Test Team 1" }),
|
||||
team({ team_id: "team-2", team_alias: "Test Team 2", models: [] }),
|
||||
];
|
||||
|
||||
vi.mocked(networking.availableTeamListCall).mockResolvedValue(mockTeams);
|
||||
|
||||
renderWithProviders(<AvailableTeamsPanel accessToken="token-123" userID="user-123" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Team 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Team 2")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should display the empty state when no teams are available", async () => {
|
||||
vi.mocked(networking.availableTeamListCall).mockResolvedValue([]);
|
||||
|
||||
renderWithProviders(<AvailableTeamsPanel accessToken="token-123" userID="user-123" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/No available teams to join/i)).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText(/See how to set available teams/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call teamMemberAddCall when the Join team menu item is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(networking.availableTeamListCall).mockResolvedValue([team({ team_id: "team-1" })]);
|
||||
vi.mocked(networking.teamMemberAddCall).mockResolvedValue({});
|
||||
|
||||
renderWithProviders(<AvailableTeamsPanel accessToken="token-123" userID="user-123" />);
|
||||
|
||||
await user.click(await screen.findByTestId("available-team-actions-team-1"));
|
||||
await user.click(await screen.findByTestId("available-team-action-join"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(networking.teamMemberAddCall).toHaveBeenCalledWith("token-123", "team-1", {
|
||||
user_id: "user-123",
|
||||
role: "user",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should show the All Proxy Models badge when a team has no models", async () => {
|
||||
vi.mocked(networking.availableTeamListCall).mockResolvedValue([team({ models: [] })]);
|
||||
|
||||
renderWithProviders(<AvailableTeamsPanel accessToken="token-123" userID="user-123" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("All Proxy Models")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should show model badges when a team has models", async () => {
|
||||
vi.mocked(networking.availableTeamListCall).mockResolvedValue([team({ models: ["gpt-4", "gpt-3.5-turbo"] })]);
|
||||
|
||||
renderWithProviders(<AvailableTeamsPanel accessToken="token-123" userID="user-123" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("gpt-4")).toBeInTheDocument();
|
||||
expect(screen.getByText("gpt-3.5-turbo")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should resolve to the empty state without fetching when there is no access token", async () => {
|
||||
renderWithProviders(<AvailableTeamsPanel accessToken={null} userID="user-123" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/No available teams to join/i)).toBeInTheDocument();
|
||||
});
|
||||
expect(networking.availableTeamListCall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should hold the loading skeleton until the fetch settles", async () => {
|
||||
let resolveFetch: (teams: AvailableTeam[]) => void = () => {};
|
||||
const pending = new Promise<AvailableTeam[]>((resolve) => {
|
||||
resolveFetch = resolve;
|
||||
});
|
||||
vi.mocked(networking.availableTeamListCall).mockReturnValue(pending);
|
||||
|
||||
renderWithProviders(<AvailableTeamsPanel accessToken="token-123" userID="user-123" />);
|
||||
|
||||
expect(screen.queryByText(/No available teams to join/i)).not.toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
resolveFetch([]);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/No available teams to join/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { availableTeamListCall, teamMemberAddCall } from "@/components/networking";
|
||||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
||||
|
||||
import AvailableTeamsTable from "./AvailableTeamsTable";
|
||||
import { AvailableTeam } from "./AvailableTeamsTableColumns";
|
||||
|
||||
interface AvailableTeamsProps {
|
||||
accessToken: string | null;
|
||||
userID: string | null;
|
||||
}
|
||||
|
||||
const AvailableTeamsPanel: React.FC<AvailableTeamsProps> = ({ accessToken, userID }) => {
|
||||
const [availableTeams, setAvailableTeams] = useState<AvailableTeam[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAvailableTeams = async () => {
|
||||
if (!accessToken || !userID) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await availableTeamListCall(accessToken);
|
||||
setAvailableTeams(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching available teams:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAvailableTeams();
|
||||
}, [accessToken, userID]);
|
||||
|
||||
const handleJoinTeam = async (teamId: string) => {
|
||||
if (!accessToken || !userID) return;
|
||||
|
||||
try {
|
||||
await teamMemberAddCall(accessToken, teamId, {
|
||||
user_id: userID,
|
||||
role: "user",
|
||||
});
|
||||
|
||||
NotificationsManager.success("Successfully joined team");
|
||||
setAvailableTeams((teams) => teams.filter((team) => team.team_id !== teamId));
|
||||
} catch (error) {
|
||||
console.error("Error joining team:", error);
|
||||
NotificationsManager.fromBackend("Failed to join team");
|
||||
}
|
||||
};
|
||||
|
||||
return <AvailableTeamsTable teams={availableTeams} isLoading={isLoading} onJoinTeam={handleJoinTeam} />;
|
||||
};
|
||||
|
||||
export default AvailableTeamsPanel;
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
"use client";
|
||||
|
||||
import { SortingState } from "@tanstack/react-table";
|
||||
import { Users } from "lucide-react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
|
||||
import { DataTable } from "@/components/shared/DataTable";
|
||||
|
||||
import { AvailableTeam, getAvailableTeamsTableColumns } from "./AvailableTeamsTableColumns";
|
||||
|
||||
interface AvailableTeamsTableProps {
|
||||
teams: AvailableTeam[];
|
||||
isLoading: boolean;
|
||||
onJoinTeam: (teamId: string) => void;
|
||||
}
|
||||
|
||||
const DEFAULT_SORTING: SortingState = [{ id: "team_alias", desc: false }];
|
||||
|
||||
function EmptyState() {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-1 py-6">
|
||||
<div className="mb-1 flex size-10 items-center justify-center rounded-lg bg-muted">
|
||||
<Users className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="text-sm font-medium text-foreground">No available teams to join</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
See how to set available teams{" "}
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/self_serve#all-settings-for-self-serve--sso-flow"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const AvailableTeamsTable: React.FC<AvailableTeamsTableProps> = ({ teams, isLoading, onJoinTeam }) => {
|
||||
const [sorting, setSorting] = useState<SortingState>(DEFAULT_SORTING);
|
||||
|
||||
const columns = useMemo(() => getAvailableTeamsTableColumns({ onJoinTeam }), [onJoinTeam]);
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
data={teams}
|
||||
columns={columns}
|
||||
getRowId={(team, index) => team.team_id || String(index)}
|
||||
sortingMode="client"
|
||||
sorting={sorting}
|
||||
onSortingChange={setSorting}
|
||||
isLoading={isLoading}
|
||||
loadingMessage="Loading available teams…"
|
||||
noDataMessage={<EmptyState />}
|
||||
size="compact"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvailableTeamsTable;
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { MoreHorizontal, UserPlus } from "lucide-react";
|
||||
|
||||
import { DataTableSortHeader } from "@/components/shared/DataTable";
|
||||
import { IdentityCell, ModelsCell } from "@/components/shared/table_cells";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { cn } from "@/lib/cva.config";
|
||||
|
||||
export interface AvailableTeam {
|
||||
team_id: string;
|
||||
team_alias: string;
|
||||
description?: string;
|
||||
models: string[];
|
||||
members_with_roles: { user_id?: string; user_email?: string; role: string }[];
|
||||
}
|
||||
|
||||
function AvailableTeamRowActions({ team, onJoinTeam }: { team: AvailableTeam; onJoinTeam: (teamId: string) => void }) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
aria-label="Open team actions"
|
||||
data-testid={`available-team-actions-${team.team_id}`}
|
||||
className={cn(buttonVariants({ variant: "ghost", size: "icon-sm" }), "text-muted-foreground")}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-44">
|
||||
<DropdownMenuItem data-testid="available-team-action-join" onClick={() => onJoinTeam(team.team_id)}>
|
||||
<UserPlus />
|
||||
Join team
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
interface AvailableTeamsTableColumnsDeps {
|
||||
onJoinTeam: (teamId: string) => void;
|
||||
}
|
||||
|
||||
export const getAvailableTeamsTableColumns = ({
|
||||
onJoinTeam,
|
||||
}: AvailableTeamsTableColumnsDeps): ColumnDef<AvailableTeam>[] => [
|
||||
{
|
||||
id: "team_alias",
|
||||
accessorKey: "team_alias",
|
||||
meta: { title: "Team Name" },
|
||||
header: ({ column }) => <DataTableSortHeader column={column} title="Team Name" />,
|
||||
size: 220,
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => (
|
||||
<IdentityCell title={row.original.team_alias} className="max-w-72" titleClassName="font-medium" />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "description",
|
||||
accessorKey: "description",
|
||||
meta: { title: "Description" },
|
||||
header: "Description",
|
||||
size: 280,
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const description = row.original.description;
|
||||
return (
|
||||
<span className="block max-w-72 truncate text-sm text-muted-foreground" title={description || undefined}>
|
||||
{description || "No description available"}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "members",
|
||||
accessorFn: (team) => team.members_with_roles.length,
|
||||
meta: { title: "Members" },
|
||||
header: ({ column }) => <DataTableSortHeader column={column} title="Members" />,
|
||||
size: 120,
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm text-muted-foreground">{row.original.members_with_roles.length} members</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "models",
|
||||
meta: { title: "Models" },
|
||||
header: "Models",
|
||||
size: 260,
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => <ModelsCell models={row.original.models} />,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
meta: { className: "text-right", headerClassName: "text-right" },
|
||||
header: () => <span className="sr-only">Actions</span>,
|
||||
size: 64,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex justify-end">
|
||||
<AvailableTeamRowActions team={row.original} onJoinTeam={onJoinTeam} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
import * as networking from "@/components/networking";
|
||||
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../../../tests/test-utils";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import AvailableTeamsPanel from "./available_teams";
|
||||
|
||||
vi.mock("@/components/networking", () => ({
|
||||
availableTeamListCall: vi.fn(),
|
||||
teamMemberAddCall: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("AvailableTeamsPanel", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render", async () => {
|
||||
vi.mocked(networking.availableTeamListCall).mockResolvedValue([]);
|
||||
|
||||
renderWithProviders(<AvailableTeamsPanel accessToken="token-123" userID="user-123" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Team Name")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should display teams when available", async () => {
|
||||
const mockTeams = [
|
||||
{
|
||||
team_id: "team-1",
|
||||
team_alias: "Test Team 1",
|
||||
description: "Test Description 1",
|
||||
models: ["gpt-4"],
|
||||
members_with_roles: [{ user_id: "user-1", user_email: "user1@test.com", role: "admin" }],
|
||||
},
|
||||
{
|
||||
team_id: "team-2",
|
||||
team_alias: "Test Team 2",
|
||||
description: "Test Description 2",
|
||||
models: [],
|
||||
members_with_roles: [{ user_id: "user-2", user_email: "user2@test.com", role: "user" }],
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(networking.availableTeamListCall).mockResolvedValue(mockTeams);
|
||||
|
||||
renderWithProviders(<AvailableTeamsPanel accessToken="token-123" userID="user-123" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Team 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Team 2")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should display empty state when no teams are available", async () => {
|
||||
vi.mocked(networking.availableTeamListCall).mockResolvedValue([]);
|
||||
|
||||
renderWithProviders(<AvailableTeamsPanel accessToken="token-123" userID="user-123" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/No available teams to join/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/See how to set available teams/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should call teamMemberAddCall when join team button is clicked", async () => {
|
||||
const mockTeams = [
|
||||
{
|
||||
team_id: "team-1",
|
||||
team_alias: "Test Team 1",
|
||||
description: "Test Description 1",
|
||||
models: ["gpt-4"],
|
||||
members_with_roles: [{ user_id: "user-1", user_email: "user1@test.com", role: "admin" }],
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(networking.availableTeamListCall).mockResolvedValue(mockTeams);
|
||||
vi.mocked(networking.teamMemberAddCall).mockResolvedValue({});
|
||||
|
||||
renderWithProviders(<AvailableTeamsPanel accessToken="token-123" userID="user-123" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Test Team 1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const joinButtons = screen.getAllByRole("button", { name: /join team/i });
|
||||
await act(async () => {
|
||||
fireEvent.click(joinButtons[0]);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(networking.teamMemberAddCall).toHaveBeenCalledWith("token-123", "team-1", {
|
||||
user_id: "user-123",
|
||||
role: "user",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should display All Proxy Models badge when team has no models", async () => {
|
||||
const mockTeams = [
|
||||
{
|
||||
team_id: "team-1",
|
||||
team_alias: "Test Team 1",
|
||||
description: "Test Description 1",
|
||||
models: [],
|
||||
members_with_roles: [{ user_id: "user-1", user_email: "user1@test.com", role: "admin" }],
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(networking.availableTeamListCall).mockResolvedValue(mockTeams);
|
||||
|
||||
renderWithProviders(<AvailableTeamsPanel accessToken="token-123" userID="user-123" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("All Proxy Models")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should display model badges when team has models", async () => {
|
||||
const mockTeams = [
|
||||
{
|
||||
team_id: "team-1",
|
||||
team_alias: "Test Team 1",
|
||||
description: "Test Description 1",
|
||||
models: ["gpt-4", "gpt-3.5-turbo"],
|
||||
members_with_roles: [{ user_id: "user-1", user_email: "user1@test.com", role: "admin" }],
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(networking.availableTeamListCall).mockResolvedValue(mockTeams);
|
||||
|
||||
renderWithProviders(<AvailableTeamsPanel accessToken="token-123" userID="user-123" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("gpt-4")).toBeInTheDocument();
|
||||
expect(screen.getByText("gpt-3.5-turbo")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
Card,
|
||||
Button,
|
||||
Text,
|
||||
Badge,
|
||||
} from "@tremor/react";
|
||||
import { availableTeamListCall, teamMemberAddCall } from "../networking";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
|
||||
interface AvailableTeam {
|
||||
team_id: string;
|
||||
team_alias: string;
|
||||
description?: string;
|
||||
models: string[];
|
||||
members_with_roles: { user_id?: string; user_email?: string; role: string }[];
|
||||
}
|
||||
|
||||
interface AvailableTeamsProps {
|
||||
accessToken: string | null;
|
||||
userID: string | null;
|
||||
}
|
||||
|
||||
const AvailableTeamsPanel: React.FC<AvailableTeamsProps> = ({ accessToken, userID }) => {
|
||||
const [availableTeams, setAvailableTeams] = useState<AvailableTeam[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAvailableTeams = async () => {
|
||||
if (!accessToken || !userID) return;
|
||||
|
||||
try {
|
||||
const response = await availableTeamListCall(accessToken);
|
||||
|
||||
setAvailableTeams(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching available teams:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchAvailableTeams();
|
||||
}, [accessToken, userID]);
|
||||
|
||||
const handleJoinTeam = async (teamId: string) => {
|
||||
if (!accessToken || !userID) return;
|
||||
|
||||
try {
|
||||
const response = await teamMemberAddCall(accessToken, teamId, {
|
||||
user_id: userID,
|
||||
role: "user",
|
||||
});
|
||||
|
||||
NotificationsManager.success("Successfully joined team");
|
||||
// Update available teams list
|
||||
setAvailableTeams((teams) => teams.filter((team) => team.team_id !== teamId));
|
||||
} catch (error) {
|
||||
console.error("Error joining team:", error);
|
||||
NotificationsManager.fromBackend("Failed to join team");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh]">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Team Name</TableHeaderCell>
|
||||
<TableHeaderCell>Description</TableHeaderCell>
|
||||
<TableHeaderCell>Members</TableHeaderCell>
|
||||
<TableHeaderCell>Models</TableHeaderCell>
|
||||
<TableHeaderCell>Actions</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{availableTeams.map((team) => (
|
||||
<TableRow key={team.team_id}>
|
||||
<TableCell>
|
||||
<Text>{team.team_alias}</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text>{team.description || "No description available"}</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text>{team.members_with_roles.length} members</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
{!team.models || team.models.length === 0 ? (
|
||||
<Badge size="xs" color="red">
|
||||
<Text>All Proxy Models</Text>
|
||||
</Badge>
|
||||
) : (
|
||||
team.models.map((model, index) => (
|
||||
<Badge key={index} size="xs" className="mb-1" color="blue">
|
||||
<Text>{model.length > 30 ? `${model.slice(0, 30)}...` : model}</Text>
|
||||
</Badge>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="xs" variant="secondary" onClick={() => handleJoinTeam(team.team_id)}>
|
||||
Join Team
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{availableTeams.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center">
|
||||
<Text>
|
||||
No available teams to join. See how to set available teams{" "}
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/self_serve#all-settings-for-self-serve--sso-flow"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:text-blue-700 underline"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvailableTeamsPanel;
|
||||
Loading…
Add table
Reference in a new issue