diff --git a/ui/litellm-dashboard/src/components/common_components/MemberTable.tsx b/ui/litellm-dashboard/src/components/common_components/MemberTable.tsx index 7af2e442b90..611f414a64e 100644 --- a/ui/litellm-dashboard/src/components/common_components/MemberTable.tsx +++ b/ui/litellm-dashboard/src/components/common_components/MemberTable.tsx @@ -22,6 +22,9 @@ export interface MemberTableProps { /** When true, renders top-right pagination controls instead of the default antd bottom pagination. */ withPagination?: boolean; defaultPageSize?: number; + /** Controlled current page (optional). When provided, pair with onPageChange. */ + currentPage?: number; + onPageChange?: (page: number) => void; } export default function MemberTable({ @@ -38,10 +41,15 @@ export default function MemberTable({ loading, withPagination = false, defaultPageSize = 50, + currentPage: controlledPage, + onPageChange, }: MemberTableProps) { - const [page, setPage] = useState(1); + const [internalPage, setInternalPage] = useState(1); const [pageSize, setPageSize] = useState(defaultPageSize); + const page = controlledPage ?? internalPage; + const setPage = onPageChange ?? setInternalPage; + const total = members.length; const totalPages = Math.max(1, Math.ceil(total / pageSize)); const safePage = Math.min(page, totalPages); diff --git a/ui/litellm-dashboard/src/components/team/TeamMemberTab.test.tsx b/ui/litellm-dashboard/src/components/team/TeamMemberTab.test.tsx index a040cbe19ca..2a67cac1203 100644 --- a/ui/litellm-dashboard/src/components/team/TeamMemberTab.test.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamMemberTab.test.tsx @@ -403,12 +403,35 @@ describe("TeamMembersComponent", () => { const roleSelect = screen.getByRole("combobox"); await user.click(roleSelect); + // "Admin" filter: only user2 (role: "admin") should remain await user.click(screen.getByText("Admin")); expect(screen.getAllByText("user2@test.com").length).toBeGreaterThanOrEqual(1); expect(screen.queryByText("user1@test.com")).not.toBeInTheDocument(); }); + it("should filter to non-admin members when Non-admin is selected", async () => { + const user = userEvent.setup(); + renderWithProviders( + , + ); + + const roleSelect = screen.getByRole("combobox"); + await user.click(roleSelect); + // "Non-admin" filter: only user1 (role: "member") should remain + await user.click(screen.getByText("Non-admin")); + + expect(screen.getAllByText("user1@test.com").length).toBeGreaterThanOrEqual(1); + expect(screen.queryByText("user2@test.com")).not.toBeInTheDocument(); + }); + it("should show all members when search is cleared", async () => { const user = userEvent.setup(); renderWithProviders( @@ -430,7 +453,7 @@ describe("TeamMembersComponent", () => { expect(screen.getAllByText("user2@test.com").length).toBeGreaterThanOrEqual(1); }); - + it("should hide action buttons when canEditTeam is false", () => { renderWithProviders( (null); + const [memberTablePage, setMemberTablePage] = useState(1); + + // Reset to page 1 when filter/search changes — without remounting MemberTable + useEffect(() => { setMemberTablePage(1); }, [searchText, roleFilter]); // O(1) lookup instead of O(n) find() per member per column const membershipsMap = useMemo( @@ -46,7 +50,12 @@ export default function TeamMemberTab({ const filteredMembers = useMemo(() => { const q = searchText.trim().toLowerCase(); return teamData.team_info.members_with_roles.filter((m) => { - if (roleFilter && m.role?.toLowerCase() !== roleFilter) return false; + if (roleFilter) { + const role = m.role?.toLowerCase() ?? ""; + const isAdmin = role === "admin" || role === "org_admin"; + if (roleFilter === "admin" && !isAdmin) return false; + if (roleFilter === "non-admin" && isAdmin) return false; + } if (!q) return true; return ( m.user_email?.toLowerCase().includes(q) || @@ -226,16 +235,17 @@ export default function TeamMemberTab({ style={{ width: 160 }} options={[ { value: "admin", label: "Admin" }, - { value: "user", label: "User" }, + { value: "non-admin", label: "Non-admin" }, ]} onChange={(v) => setRoleFilter(v ?? null)} /> { const membership = membershipsMap.get(record.user_id ?? ""); const enhancedMember = {