mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
fix(ui): explain why team member reset spend is unavailable instead of hiding it (#44629)
Co-authored-by: yassin <yassin@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
cad87a900f
commit
f0415ee033
5 changed files with 75 additions and 27 deletions
|
|
@ -84,6 +84,23 @@ describe("TableIconActionButton", () => {
|
|||
|
||||
await user.hover(screen.getByTestId("test-button"));
|
||||
|
||||
expect(await screen.findByText("Cannot edit")).toBeInTheDocument();
|
||||
});
|
||||
it("should show disabledTooltipText on keyboard focus when disabled", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<TableIconActionButton
|
||||
variant="Edit"
|
||||
onClick={() => {}}
|
||||
dataTestId="test-button"
|
||||
disabled
|
||||
tooltipText="Edit"
|
||||
disabledTooltipText="Cannot edit"
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.tab();
|
||||
|
||||
expect(await screen.findByText("Cannot edit")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export default function TableIconActionButton({
|
|||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<span />}>{button}</TooltipTrigger>
|
||||
<TooltipTrigger render={<span tabIndex={disabled ? 0 : undefined} />}>{button}</TooltipTrigger>
|
||||
<TooltipContent>{title}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export interface MemberTableProps {
|
|||
extraColumns?: MemberTableColumn[];
|
||||
showDeleteForMember?: (member: Member) => boolean;
|
||||
onResetSpend?: (member: Member) => void;
|
||||
showResetSpendForMember?: (member: Member) => boolean;
|
||||
resetSpendDisabledReason?: (member: Member) => string | null;
|
||||
emptyText?: string;
|
||||
}
|
||||
|
||||
|
|
@ -66,6 +66,19 @@ function RoleHeaderTitle({ title, tooltip }: { title: string; tooltip?: string }
|
|||
);
|
||||
}
|
||||
|
||||
function ResetSpendAction({ disabledReason, onClick }: { disabledReason: string | null; onClick: () => void }) {
|
||||
return (
|
||||
<TableIconActionButton
|
||||
variant="Reset"
|
||||
tooltipText="Reset spend"
|
||||
disabled={disabledReason !== null}
|
||||
disabledTooltipText={disabledReason ?? undefined}
|
||||
dataTestId="reset-member-spend"
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const ACTIONS_COLUMN_WIDTH = 120;
|
||||
|
||||
interface MemberColumnDeps {
|
||||
|
|
@ -77,7 +90,7 @@ interface MemberColumnDeps {
|
|||
extraColumns: MemberTableColumn[];
|
||||
showDeleteForMember?: (member: Member) => boolean;
|
||||
onResetSpend?: (member: Member) => void;
|
||||
showResetSpendForMember?: (member: Member) => boolean;
|
||||
resetSpendDisabledReason?: (member: Member) => string | null;
|
||||
}
|
||||
|
||||
const extraColumnDef = (column: MemberTableColumn): ColumnDef<Member> => {
|
||||
|
|
@ -113,7 +126,7 @@ const buildColumns = ({
|
|||
extraColumns,
|
||||
showDeleteForMember,
|
||||
onResetSpend,
|
||||
showResetSpendForMember,
|
||||
resetSpendDisabledReason,
|
||||
}: MemberColumnDeps): ColumnDef<Member>[] => [
|
||||
{
|
||||
id: "user_alias",
|
||||
|
|
@ -182,11 +195,9 @@ const buildColumns = ({
|
|||
dataTestId="edit-member"
|
||||
onClick={() => onEdit(row.original)}
|
||||
/>
|
||||
{onResetSpend && (showResetSpendForMember?.(row.original) ?? true) && (
|
||||
<TableIconActionButton
|
||||
variant="Reset"
|
||||
tooltipText="Reset spend"
|
||||
dataTestId="reset-member-spend"
|
||||
{onResetSpend && (
|
||||
<ResetSpendAction
|
||||
disabledReason={resetSpendDisabledReason?.(row.original) ?? null}
|
||||
onClick={() => onResetSpend(row.original)}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -214,7 +225,7 @@ export default function MemberTable({
|
|||
extraColumns = [],
|
||||
showDeleteForMember,
|
||||
onResetSpend,
|
||||
showResetSpendForMember,
|
||||
resetSpendDisabledReason,
|
||||
emptyText,
|
||||
}: MemberTableProps) {
|
||||
const [globalFilter, setGlobalFilter] = useState("");
|
||||
|
|
@ -230,7 +241,7 @@ export default function MemberTable({
|
|||
extraColumns,
|
||||
showDeleteForMember,
|
||||
onResetSpend,
|
||||
showResetSpendForMember,
|
||||
resetSpendDisabledReason,
|
||||
};
|
||||
const columns = buildColumns(columnDeps);
|
||||
const roleFilterItems = [
|
||||
|
|
|
|||
|
|
@ -578,7 +578,7 @@ describe("TeamMembersComponent", () => {
|
|||
POST.mockRejectedValue(new Error("Cannot reset your own spend. Ask a proxy admin."));
|
||||
renderEditableTab();
|
||||
|
||||
await user.click(screen.getByTestId("reset-member-spend"));
|
||||
await user.click(within(screen.getByRole("row", { name: /user1@test\.com/ })).getByTestId("reset-member-spend"));
|
||||
const dialog = await screen.findByRole("dialog", { name: "Reset Team Member Spend" });
|
||||
await user.click(within(dialog).getByRole("button", { name: "Reset" }));
|
||||
|
||||
|
|
@ -591,7 +591,7 @@ describe("TeamMembersComponent", () => {
|
|||
const user = userEvent.setup();
|
||||
renderEditableTab();
|
||||
|
||||
await user.click(screen.getByTestId("reset-member-spend"));
|
||||
await user.click(within(screen.getByRole("row", { name: /user1@test\.com/ })).getByTestId("reset-member-spend"));
|
||||
const dialog = await screen.findByRole("dialog", { name: "Reset Team Member Spend" });
|
||||
await user.click(within(dialog).getByRole("button", { name: "Cancel" }));
|
||||
|
||||
|
|
@ -599,31 +599,47 @@ describe("TeamMembersComponent", () => {
|
|||
expect(POST).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("only offers the reset on members that have current cycle spend", () => {
|
||||
it("disables the reset with a reason on members that have no current cycle spend", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderEditableTab();
|
||||
|
||||
expect(
|
||||
within(screen.getByRole("row", { name: /user1@test\.com/ })).getByTestId("reset-member-spend"),
|
||||
).toBeVisible();
|
||||
expect(
|
||||
within(screen.getByRole("row", { name: /user2@test\.com/ })).queryByTestId("reset-member-spend"),
|
||||
).not.toBeInTheDocument();
|
||||
const resetButton = within(screen.getByRole("row", { name: /user2@test\.com/ })).getByTestId(
|
||||
"reset-member-spend",
|
||||
);
|
||||
await user.hover(resetButton);
|
||||
expect(await screen.findByText("No current cycle spend to reset")).toBeInTheDocument();
|
||||
|
||||
await user.click(resetButton);
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides the reset on the caller's own row for a team admin, since the backend rejects it", () => {
|
||||
it("disables the reset with a reason on the caller's own row for a team admin, since the backend rejects it", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(useAuthorized).mockReturnValue({ userId: "user1@test.com", userRole: "Internal User" } as never);
|
||||
vi.mocked(isProxyAdminRole).mockReturnValue(false);
|
||||
renderEditableTab();
|
||||
|
||||
expect(screen.queryByTestId("reset-member-spend")).not.toBeInTheDocument();
|
||||
const resetButton = within(screen.getByRole("row", { name: /user1@test\.com/ })).getByTestId(
|
||||
"reset-member-spend",
|
||||
);
|
||||
await user.hover(resetButton);
|
||||
expect(await screen.findByText("Ask a proxy admin to reset your own spend")).toBeInTheDocument();
|
||||
|
||||
await user.click(resetButton);
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the reset on the caller's own row for a proxy admin", () => {
|
||||
it("allows the reset on the caller's own row for a proxy admin", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(useAuthorized).mockReturnValue({ userId: "user1@test.com", userRole: "Admin" } as never);
|
||||
vi.mocked(isProxyAdminRole).mockReturnValue(true);
|
||||
renderEditableTab();
|
||||
|
||||
expect(screen.getByTestId("reset-member-spend")).toBeVisible();
|
||||
await user.click(within(screen.getByRole("row", { name: /user1@test\.com/ })).getByTestId("reset-member-spend"));
|
||||
|
||||
expect(await screen.findByRole("dialog", { name: "Reset Team Member Spend" })).toHaveTextContent(
|
||||
"user1@test.com",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -291,6 +291,12 @@ export default function TeamMemberTab({
|
|||
);
|
||||
};
|
||||
|
||||
const resetSpendDisabledReason = (member: Member): string | null => {
|
||||
if (!isProxyAdmin && member.user_id === userId) return "Ask a proxy admin to reset your own spend";
|
||||
if (getUserCurrentCycleSpend(member.user_id) <= 0) return "No current cycle spend to reset";
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MemberTable
|
||||
|
|
@ -311,9 +317,7 @@ export default function TeamMemberTab({
|
|||
isProxyAdmin || (canEditTeam && !isUserTeamAdmin) || (isUserTeamAdmin && !disableTeamAdminDeleteTeamUser)
|
||||
}
|
||||
onResetSpend={setMemberToResetSpend}
|
||||
showResetSpendForMember={(record) =>
|
||||
getUserCurrentCycleSpend(record.user_id) > 0 && (isProxyAdmin || record.user_id !== userId)
|
||||
}
|
||||
resetSpendDisabledReason={resetSpendDisabledReason}
|
||||
/>
|
||||
<Dialog open={memberToResetSpend !== null} onOpenChange={(open) => !open && setMemberToResetSpend(null)}>
|
||||
<DialogContent>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue