mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
Migrate teams route chrome to shadcn (Phase 1)
- TeamsView.tsx: drop antd Form.useForm placeholders (never passed down), drop Tremor Card/Button/Col/Text/Grid -> Tailwind/shadcn. Keep Tremor TabPanel (still-required container for TeamsHeaderTabs which wraps Tremor TabGroup). - CreateTeamModal.tsx: full RHF + Dialog + shadcn rewrite. Form.useForm -> useForm/FormProvider; Modal -> Dialog; Tremor Accordion -> shadcn Accordion; antd Select (single / multiple / tags) -> shadcn Select + custom MultiSelect/TagMultiSelect; antd Switch -> shadcn Switch; antd Input.TextArea -> shadcn Textarea; antd Tooltip -> shadcn Tooltip; Select2.Option trees -> SelectItem. Child selectors (VectorStoreSelector, MCPServerSelector, AgentSelector, MCPToolPermissions) drive Controllers instead of antd form.setFieldValue. - TeamsTable.test.tsx / TeamsHeaderTabs.test.tsx: repair sibling tests. Drop obsolete antd and @heroicons mocks; rewrite TeamsTable admin/non-admin button assertions to semantic getByRole queries against aria-labeled shadcn buttons. Notes: Tremor TabGroup/TabList/Tab/TabPanels still used inside TeamsHeaderTabs pending its own section (chrome-only imports kept behind eslint-disable directive already in the file). ModelsCell.test.tsx failures are pre-existing and out of scope. Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
43337912e1
commit
7dae8f73aa
4 changed files with 1051 additions and 586 deletions
|
|
@ -3,12 +3,13 @@ import { useQueryClient } from "@tanstack/react-query";
|
|||
import { organizationKeys } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
|
||||
import { teamDeleteCall, Organization } from "@/components/networking";
|
||||
import { fetchTeams } from "@/components/common_components/fetch_teams";
|
||||
import { Form } from "antd";
|
||||
import TeamInfoView from "@/components/team/TeamInfo";
|
||||
import TeamSSOSettings from "@/components/TeamSSOSettings";
|
||||
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
|
||||
import { isAdminRole } from "@/utils/roles";
|
||||
import { Card, Button, Col, Text, Grid, TabPanel } from "@tremor/react";
|
||||
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
|
||||
import { TabPanel } from "@tremor/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import AvailableTeamsPanel from "@/components/team/available_teams";
|
||||
import type { KeyResponse, Team } from "@/components/key_team_helpers/key_list";
|
||||
|
||||
|
|
@ -68,15 +69,10 @@ const TeamsView: React.FC<TeamProps> = ({
|
|||
sort_order: "desc",
|
||||
});
|
||||
|
||||
const [form] = Form.useForm();
|
||||
const [memberForm] = Form.useForm();
|
||||
|
||||
const [selectedTeamId, setSelectedTeamId] = useState<string | null>(null);
|
||||
const [editTeam, setEditTeam] = useState<boolean>(false);
|
||||
|
||||
const [isTeamModalVisible, setIsTeamModalVisible] = useState(false);
|
||||
const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false);
|
||||
const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false);
|
||||
const [userModels, setUserModels] = useState<string[]>([]);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [teamToDelete, setTeamToDelete] = useState<string | null>(null);
|
||||
|
|
@ -111,26 +107,17 @@ const TeamsView: React.FC<TeamProps> = ({
|
|||
|
||||
const handleOk = () => {
|
||||
setIsTeamModalVisible(false);
|
||||
form.resetFields();
|
||||
setLoggingSettings([]);
|
||||
setModelAliases({});
|
||||
};
|
||||
|
||||
const handleMemberOk = () => {
|
||||
setIsAddMemberModalVisible(false);
|
||||
setIsEditMemberModalVisible(false);
|
||||
memberForm.resetFields();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsTeamModalVisible(false);
|
||||
form.resetFields();
|
||||
setLoggingSettings([]);
|
||||
setModelAliases({});
|
||||
};
|
||||
|
||||
const handleDelete = async (team_id: string) => {
|
||||
// Set the team to delete and open the confirmation modal
|
||||
setTeamToDelete(team_id);
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
|
|
@ -143,20 +130,16 @@ const TeamsView: React.FC<TeamProps> = ({
|
|||
try {
|
||||
await teamDeleteCall(accessToken, teamToDelete);
|
||||
queryClient.invalidateQueries({ queryKey: organizationKeys.all });
|
||||
// Successfully completed the deletion. Update the state to trigger a rerender.
|
||||
fetchTeams(accessToken, userID, userRole, currentOrg, setTeams);
|
||||
} catch (error) {
|
||||
console.error("Error deleting the team:", error);
|
||||
// Handle any error situations, such as displaying an error message to the user.
|
||||
}
|
||||
|
||||
// Close the confirmation modal and reset the teamToDelete
|
||||
setIsDeleteModalOpen(false);
|
||||
setTeamToDelete(null);
|
||||
};
|
||||
|
||||
const cancelDelete = () => {
|
||||
// Close the confirmation modal and reset the teamToDelete
|
||||
setIsDeleteModalOpen(false);
|
||||
setTeamToDelete(null);
|
||||
};
|
||||
|
|
@ -177,7 +160,6 @@ const TeamsView: React.FC<TeamProps> = ({
|
|||
const handleFilterChange = (key: keyof FilterState, value: string) => {
|
||||
const newFilters = { ...filters, [key]: value };
|
||||
setFilters(newFilters);
|
||||
// Call teamListCall with the new filters
|
||||
if (accessToken) {
|
||||
v2TeamListCall(
|
||||
accessToken,
|
||||
|
|
@ -204,7 +186,6 @@ const TeamsView: React.FC<TeamProps> = ({
|
|||
sort_order: sortOrder,
|
||||
};
|
||||
setFilters(newFilters);
|
||||
// Call teamListCall with the new sort parameters
|
||||
if (accessToken) {
|
||||
v2TeamListCall(
|
||||
accessToken,
|
||||
|
|
@ -232,7 +213,6 @@ const TeamsView: React.FC<TeamProps> = ({
|
|||
sort_by: "created_at",
|
||||
sort_order: "desc",
|
||||
});
|
||||
// Reset teams list
|
||||
if (accessToken) {
|
||||
v2TeamListCall(accessToken, null, userID || null, null, null)
|
||||
.then((response) => {
|
||||
|
|
@ -248,8 +228,8 @@ const TeamsView: React.FC<TeamProps> = ({
|
|||
|
||||
return (
|
||||
<div className="w-full mx-4 h-[75vh]">
|
||||
<Grid numItems={1} className="gap-2 p-8 w-full mt-2">
|
||||
<Col numColSpan={1} className="flex flex-col gap-2">
|
||||
<div className="grid grid-cols-1 gap-2 p-8 w-full mt-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
{(userRole == "Admin" || userRole == "Org Admin") && (
|
||||
<Button className="w-fit" onClick={() => setIsTeamModalVisible(true)}>
|
||||
+ Create New Team
|
||||
|
|
@ -269,7 +249,6 @@ const TeamsView: React.FC<TeamProps> = ({
|
|||
}
|
||||
return team;
|
||||
});
|
||||
// Minimal fix: refresh the full team list after an update
|
||||
if (accessToken) {
|
||||
fetchTeams(accessToken, userID, userRole, currentOrg, setTeams);
|
||||
}
|
||||
|
|
@ -296,11 +275,11 @@ const TeamsView: React.FC<TeamProps> = ({
|
|||
) : (
|
||||
<TeamsHeaderTabs lastRefreshed={lastRefreshed} onRefresh={handleRefreshClick} userRole={userRole}>
|
||||
<TabPanel>
|
||||
<Text>
|
||||
<p className="text-sm">
|
||||
Click on “Team ID” to view team details <b>and</b> manage team members.
|
||||
</Text>
|
||||
<Grid numItems={1} className="gap-2 pt-2 pb-2 h-[75vh] w-full mt-2">
|
||||
<Col numColSpan={1}>
|
||||
</p>
|
||||
<div className="grid grid-cols-1 gap-2 pt-2 pb-2 h-[75vh] w-full mt-2">
|
||||
<div>
|
||||
<Card className="w-full mx-auto flex-auto overflow-hidden overflow-y-auto max-h-[50vh]">
|
||||
<div className="border-b px-6 py-4">
|
||||
<div className="flex flex-col space-y-4">
|
||||
|
|
@ -333,8 +312,8 @@ const TeamsView: React.FC<TeamProps> = ({
|
|||
/>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<AvailableTeamsPanel accessToken={accessToken} userID={userID} />
|
||||
|
|
@ -362,8 +341,8 @@ const TeamsView: React.FC<TeamProps> = ({
|
|||
setIsTeamModalVisible={setIsTeamModalVisible}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,10 +12,6 @@ vi.mock("@tremor/react", () => ({
|
|||
Icon: ({ onClick, ...props }: any) => <button data-testid="refresh-icon" onClick={onClick} />,
|
||||
}));
|
||||
|
||||
vi.mock("@heroicons/react/outline", () => ({
|
||||
RefreshIcon: () => <svg data-testid="refresh-svg" />,
|
||||
}));
|
||||
|
||||
const renderTabs = (props: Partial<Parameters<typeof TeamsHeaderTabs>[0]> = {}) => {
|
||||
const defaults = {
|
||||
lastRefreshed: "",
|
||||
|
|
|
|||
|
|
@ -6,26 +6,12 @@ import { Team } from "@/components/key_team_helpers/key_list";
|
|||
import TeamsTable from "./TeamsTable";
|
||||
|
||||
vi.mock("@tremor/react", () => ({
|
||||
Button: React.forwardRef<HTMLButtonElement, any>(({ children, ...props }, ref) =>
|
||||
React.createElement("button", { ...props, ref }, children),
|
||||
),
|
||||
Icon: ({ onClick, ...props }: any) => <button data-testid={props["data-testid"] || "icon-btn"} onClick={onClick} aria-label={props["aria-label"]} />,
|
||||
Table: ({ children }: any) => <table>{children}</table>,
|
||||
TableHead: ({ children }: any) => <thead>{children}</thead>,
|
||||
TableBody: ({ children }: any) => <tbody>{children}</tbody>,
|
||||
TableRow: ({ children }: any) => <tr>{children}</tr>,
|
||||
TableHeaderCell: ({ children }: any) => <th>{children}</th>,
|
||||
TableCell: ({ children, ...props }: any) => <td {...props}>{children}</td>,
|
||||
Text: ({ children }: any) => <span>{children}</span>,
|
||||
}));
|
||||
|
||||
vi.mock("antd", () => ({
|
||||
Tooltip: ({ children }: any) => <>{children}</>,
|
||||
}));
|
||||
|
||||
vi.mock("@heroicons/react/outline", () => ({
|
||||
PencilAltIcon: () => <svg data-testid="pencil-icon" />,
|
||||
TrashIcon: () => <svg data-testid="trash-icon" />,
|
||||
}));
|
||||
|
||||
vi.mock("@/utils/dataUtils", () => ({
|
||||
|
|
@ -103,18 +89,26 @@ describe("TeamsTable", () => {
|
|||
expect(screen.getByText("org-1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show edit and delete icons for Admin users", () => {
|
||||
it("should show edit and delete buttons for Admin users", () => {
|
||||
renderTable({ userRole: "Admin" });
|
||||
|
||||
expect(screen.getAllByTestId("icon-btn").length).toBeGreaterThanOrEqual(2);
|
||||
expect(
|
||||
screen.getByRole("button", { name: /edit team/i }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: /delete team/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show edit and delete icons for non-Admin users", () => {
|
||||
it("should not show edit and delete buttons for non-Admin users", () => {
|
||||
renderTable({ userRole: "Internal User" });
|
||||
|
||||
// Only the team ID button should be present, no icon-btn for edit/delete
|
||||
const iconBtns = screen.queryAllByTestId("icon-btn");
|
||||
expect(iconBtns).toHaveLength(0);
|
||||
expect(
|
||||
screen.queryByRole("button", { name: /edit team/i }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: /delete team/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call setSelectedTeamId when team ID button is clicked", async () => {
|
||||
|
|
@ -122,7 +116,7 @@ describe("TeamsTable", () => {
|
|||
const setSelectedTeamId = vi.fn();
|
||||
renderTable({ setSelectedTeamId });
|
||||
|
||||
await user.click(screen.getByText("team-ab..."));
|
||||
await user.click(screen.getByTestId("team-id-cell"));
|
||||
|
||||
expect(setSelectedTeamId).toHaveBeenCalledWith("team-abc1234");
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue