mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(ui): address Greptile P2 — double setPage, stale page reset, filtered member count
- MemberTable: skip redundant setPage(p) when page size changes (only call setPage(1)) - TeamMemberTab: reset page synchronously in event handlers instead of useEffect, eliminating the one-render stale page; removes useEffect import - MemberTable: accept optional totalMembers prop; show "X of Y Members" when a search/filter is active so users can see total team size without clearing the filter - Add test: "1 of 2 Members" label appears when search narrows results Co-Authored-By: Claude Sonnet 4 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4b3a170d38
commit
a3d496ad58
3 changed files with 39 additions and 9 deletions
|
|
@ -25,6 +25,8 @@ export interface MemberTableProps {
|
|||
/** Controlled current page (optional). When provided, pair with onPageChange. */
|
||||
currentPage?: number;
|
||||
onPageChange?: (page: number) => void;
|
||||
/** Total unfiltered member count. When provided and differs from members.length, shows "X of Y Members". */
|
||||
totalMembers?: number;
|
||||
}
|
||||
|
||||
export default function MemberTable({
|
||||
|
|
@ -43,6 +45,7 @@ export default function MemberTable({
|
|||
defaultPageSize = 50,
|
||||
currentPage: controlledPage,
|
||||
onPageChange,
|
||||
totalMembers,
|
||||
}: MemberTableProps) {
|
||||
const [internalPage, setInternalPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(defaultPageSize);
|
||||
|
|
@ -54,6 +57,9 @@ export default function MemberTable({
|
|||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
const safePage = Math.min(page, totalPages);
|
||||
const pagedMembers = withPagination ? members.slice((safePage - 1) * pageSize, safePage * pageSize) : members;
|
||||
const countDisplay = totalMembers !== undefined && total !== totalMembers
|
||||
? `${total} of ${totalMembers} Members`
|
||||
: `${total} Member${total !== 1 ? "s" : ""}`;
|
||||
const baseColumns: ColumnsType<Member> = [
|
||||
{
|
||||
title: "User Email",
|
||||
|
|
@ -128,7 +134,7 @@ export default function MemberTable({
|
|||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<span className="text-sm text-gray-700">
|
||||
{total} Member{total !== 1 ? "s" : ""}
|
||||
{countDisplay}
|
||||
</span>
|
||||
{withPagination && (
|
||||
<Pagination
|
||||
|
|
@ -139,7 +145,14 @@ export default function MemberTable({
|
|||
showSizeChanger
|
||||
pageSizeOptions={["10", "25", "50", "100"]}
|
||||
showQuickJumper
|
||||
onChange={(p, ps) => { setPage(p); if (ps !== pageSize) { setPageSize(ps); setPage(1); } }}
|
||||
onChange={(p, ps) => {
|
||||
if (ps !== pageSize) {
|
||||
setPageSize(ps);
|
||||
setPage(1);
|
||||
} else {
|
||||
setPage(p);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -453,6 +453,25 @@ describe("TeamMembersComponent", () => {
|
|||
expect(screen.getAllByText("user2@test.com").length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("should show filtered count when search is active", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<TeamMembersComponent
|
||||
teamData={createMockTeamData()}
|
||||
canEditTeam={false}
|
||||
handleMemberDelete={mockHandleMemberDelete}
|
||||
setSelectedEditMember={mockSetSelectedEditMember}
|
||||
setIsEditMemberModalVisible={mockSetIsEditMemberModalVisible}
|
||||
setIsAddMemberModalVisible={mockSetIsAddMemberModalVisible}
|
||||
/>,
|
||||
);
|
||||
|
||||
const searchInput = screen.getByPlaceholderText(/search by email or user id/i);
|
||||
await user.type(searchInput, "user1");
|
||||
|
||||
expect(screen.getByText("1 of 2 Members")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should hide action buttons when canEditTeam is false", () => {
|
||||
renderWithProviders(
|
||||
<TeamMembersComponent
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { Input, Select, Space, Tooltip, Typography } from "antd";
|
|||
import type { ColumnsType } from "antd/es/table";
|
||||
import MemberTable from "@/components/common_components/MemberTable";
|
||||
import { TeamData, TeamMembership } from "./TeamInfo";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
interface TeamMemberTabProps {
|
||||
teamData: TeamData;
|
||||
|
|
@ -33,9 +33,6 @@ export default function TeamMemberTab({
|
|||
const [roleFilter, setRoleFilter] = useState<string | null>(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(
|
||||
() =>
|
||||
|
|
@ -226,8 +223,8 @@ export default function TeamMemberTab({
|
|||
placeholder="Search by email or user ID"
|
||||
allowClear
|
||||
style={{ width: 280 }}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
onSearch={(v) => setSearchText(v)}
|
||||
onChange={(e) => { setSearchText(e.target.value); setMemberTablePage(1); }}
|
||||
onSearch={(v) => { setSearchText(v); setMemberTablePage(1); }}
|
||||
/>
|
||||
<Select
|
||||
placeholder="Filter by role"
|
||||
|
|
@ -237,11 +234,12 @@ export default function TeamMemberTab({
|
|||
{ value: "admin", label: "Admin" },
|
||||
{ value: "non-admin", label: "Non-admin" },
|
||||
]}
|
||||
onChange={(v) => setRoleFilter(v ?? null)}
|
||||
onChange={(v) => { setRoleFilter(v ?? null); setMemberTablePage(1); }}
|
||||
/>
|
||||
</Space>
|
||||
<MemberTable
|
||||
members={filteredMembers}
|
||||
totalMembers={teamData.team_info.members_with_roles.length}
|
||||
canEdit={canEditTeam}
|
||||
withPagination
|
||||
currentPage={memberTablePage}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue