diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 5e322598a10..53768c6f945 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -2372,11 +2372,6 @@ "count": 2 } }, - "src/components/common_components/AutoRotationView.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/common_components/DefaultProxyAdminTag.tsx": { "no-restricted-imports": { "count": 1 @@ -2395,16 +2390,6 @@ "count": 1 } }, - "src/components/common_components/IconActionButton/BaseActionButton.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/components/common_components/IconActionButton/TableIconActionButtons/TableIconActionButton.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/common_components/KeyLifecycleSettings.tsx": { "local/no-complex-jsx-arrow": { "count": 1 @@ -2413,14 +2398,9 @@ "count": 2 } }, - "src/components/common_components/LabeledField.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/common_components/MemberTable.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 } }, "src/components/common_components/MetadataKeyValueFields.test.tsx": { @@ -2449,11 +2429,6 @@ "count": 1 } }, - "src/components/common_components/NewBadge.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/common_components/OrganizationDropdown.tsx": { "local/no-complex-jsx-arrow": { "count": 1 diff --git a/ui/litellm-dashboard/src/components/common_components/AutoRotationView.tsx b/ui/litellm-dashboard/src/components/common_components/AutoRotationView.tsx index d6445d1472d..4e8e0dc8121 100644 --- a/ui/litellm-dashboard/src/components/common_components/AutoRotationView.tsx +++ b/ui/litellm-dashboard/src/components/common_components/AutoRotationView.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Text, Badge } from "@tremor/react"; +import { StatusBadge } from "@/components/shared/table_cells"; import { RefreshIcon, ClockIcon } from "@heroicons/react/outline"; interface AutoRotationViewProps { @@ -38,63 +38,55 @@ const AutoRotationView: React.FC = ({ const content = (
- {/* Status Section */}
- Auto-Rotation - - {autoRotate ? "Enabled" : "Disabled"} - +

Auto-Rotation

+ {autoRotate && rotationInterval && ( <> - - Every {rotationInterval} +

+

Every {rotationInterval}

)}
- {/* Rotation History - Show if there's any rotation data OR if auto-rotation is enabled */} {(autoRotate || lastRotationAt || keyRotationAt || nextRotationAt) && (
- {/* Last Rotation - Show when available */} {lastRotationAt && ( -
- +
+
- Last Rotation - {formatTimestamp(lastRotationAt)} +

Last Rotation

+

{formatTimestamp(lastRotationAt)}

)} - {/* Next Scheduled Rotation - Show when available */} {(keyRotationAt || nextRotationAt) && ( -
- +
+
- Next Scheduled Rotation - {formatTimestamp(nextRotationAt || keyRotationAt || "")} +

Next Scheduled Rotation

+

{formatTimestamp(nextRotationAt || keyRotationAt || "")}

)} - {/* No rotation data message - Only show if auto-rotation is enabled but no data */} {autoRotate && !lastRotationAt && !keyRotationAt && !nextRotationAt && ( -
- - No rotation history available +
+ +

No rotation history available

)}
)} - {/* Disabled State - Only show if auto-rotation is disabled AND there's no rotation history */} {!autoRotate && !lastRotationAt && !keyRotationAt && !nextRotationAt && ( -
- - Auto-rotation is not enabled for this key +
+ +

Auto-rotation is not enabled for this key

)}
@@ -102,11 +94,11 @@ const AutoRotationView: React.FC = ({ if (variant === "card") { return ( -
-
+
+
- Auto-Rotation - Automatic key rotation settings and status for this key +

Auto-Rotation

+

Automatic key rotation settings and status for this key

{content} @@ -116,7 +108,7 @@ const AutoRotationView: React.FC = ({ return (
- Auto-Rotation +

Auto-Rotation

{content}
); diff --git a/ui/litellm-dashboard/src/components/common_components/IconActionButton/BaseActionButton.tsx b/ui/litellm-dashboard/src/components/common_components/IconActionButton/BaseActionButton.tsx index 1d6aa0ec73e..7e5f3fe5342 100644 --- a/ui/litellm-dashboard/src/components/common_components/IconActionButton/BaseActionButton.tsx +++ b/ui/litellm-dashboard/src/components/common_components/IconActionButton/BaseActionButton.tsx @@ -1,5 +1,4 @@ import { cx } from "@/lib/cva.config"; -import { Icon } from "@tremor/react"; import React from "react"; interface BaseActionButtonProps { @@ -10,16 +9,27 @@ interface BaseActionButtonProps { dataTestId?: string; } -export default function BaseActionButton({ icon, onClick, className, disabled, dataTestId }: BaseActionButtonProps) { +export default function BaseActionButton({ + icon: Icon, + onClick, + className, + disabled, + dataTestId, +}: BaseActionButtonProps) { return disabled ? ( - - ) : ( - + > + + + ) : ( + + + ); } diff --git a/ui/litellm-dashboard/src/components/common_components/IconActionButton/TableIconActionButtons/TableIconActionButton.test.tsx b/ui/litellm-dashboard/src/components/common_components/IconActionButton/TableIconActionButtons/TableIconActionButton.test.tsx index 718c458b455..9793a0e19ab 100644 --- a/ui/litellm-dashboard/src/components/common_components/IconActionButton/TableIconActionButtons/TableIconActionButton.test.tsx +++ b/ui/litellm-dashboard/src/components/common_components/IconActionButton/TableIconActionButtons/TableIconActionButton.test.tsx @@ -1,5 +1,6 @@ -import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; import TableIconActionButton, { TableIconActionButtonMap } from "./TableIconActionButton"; describe("TableIconActionButton", () => { @@ -12,27 +13,32 @@ describe("TableIconActionButton", () => { }); }); - it("should have a tooltip", () => { - render( {}} dataTestId="test-button" tooltipText="Edit" />); - const button = screen.getByTestId("test-button"); - const tooltipWrapper = button.closest("span"); - expect(tooltipWrapper).toBeInTheDocument(); + it("should call onClick when clicked", async () => { + const user = userEvent.setup(); + const onClick = vi.fn(); + render(); + + await user.click(screen.getByTestId("test-button")); + + expect(onClick).toHaveBeenCalledTimes(1); }); - it("should show tooltip when tooltipText is provided", async () => { + it("should not show the tooltip before the button is hovered", () => { render( {}} dataTestId="test-button" tooltipText="Edit item" />, ); - const button = screen.getByTestId("test-button"); - const buttonWrapper = button.closest("span"); + expect(screen.queryByText("Edit item")).not.toBeInTheDocument(); + }); - act(() => { - fireEvent.mouseEnter(buttonWrapper!); - }); + it("should show tooltip when tooltipText is provided", async () => { + const user = userEvent.setup(); + render( + {}} dataTestId="test-button" tooltipText="Edit item" />, + ); - await waitFor(() => { - expect(screen.getByText("Edit item")).toBeInTheDocument(); - }); + await user.hover(screen.getByTestId("test-button")); + + expect(await screen.findByText("Edit item")).toBeInTheDocument(); }); it("should render disabled state with disabled styling", () => { @@ -44,7 +50,27 @@ describe("TableIconActionButton", () => { expect(button).toHaveClass("cursor-not-allowed"); }); + it("should not call onClick when disabled", async () => { + const user = userEvent.setup(); + const onClick = vi.fn(); + render( + , + ); + + await user.click(screen.getByTestId("test-button")); + + expect(onClick).not.toHaveBeenCalled(); + }); + it("should show disabledTooltipText when disabled and disabledTooltipText is provided", async () => { + const user = userEvent.setup(); render( { disabledTooltipText="Cannot edit" />, ); - const button = screen.getByTestId("test-button"); - const buttonWrapper = button.closest("span"); - act(() => { - fireEvent.mouseEnter(buttonWrapper!); - }); + await user.hover(screen.getByTestId("test-button")); - await waitFor(() => { - expect(screen.getByText("Cannot edit")).toBeInTheDocument(); - }); + expect(await screen.findByText("Cannot edit")).toBeInTheDocument(); }); }); diff --git a/ui/litellm-dashboard/src/components/common_components/IconActionButton/TableIconActionButtons/TableIconActionButton.tsx b/ui/litellm-dashboard/src/components/common_components/IconActionButton/TableIconActionButtons/TableIconActionButton.tsx index 2f146aab723..9eeb6922116 100644 --- a/ui/litellm-dashboard/src/components/common_components/IconActionButton/TableIconActionButtons/TableIconActionButton.tsx +++ b/ui/litellm-dashboard/src/components/common_components/IconActionButton/TableIconActionButtons/TableIconActionButton.tsx @@ -1,3 +1,4 @@ +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { PencilAltIcon, PlayIcon, @@ -8,7 +9,6 @@ import { ExternalLinkIcon, ClipboardCopyIcon, } from "@heroicons/react/outline"; -import { Tooltip } from "antd"; import BaseActionButton from "../BaseActionButton"; export interface TableIconActionButtonProps { @@ -45,17 +45,21 @@ export default function TableIconActionButton({ variant, }: TableIconActionButtonProps) { const { icon, className } = TableIconActionButtonMap[variant]; + const title = disabled ? disabledTooltipText : tooltipText; + const button = ( + + ); + + if (!title) { + return {button}; + } + return ( - - - - - + + + }>{button} + {title} + + ); } diff --git a/ui/litellm-dashboard/src/components/common_components/LabeledField.test.tsx b/ui/litellm-dashboard/src/components/common_components/LabeledField.test.tsx index db6eb3356a8..a55381b344f 100644 --- a/ui/litellm-dashboard/src/components/common_components/LabeledField.test.tsx +++ b/ui/litellm-dashboard/src/components/common_components/LabeledField.test.tsx @@ -32,18 +32,22 @@ describe("LabeledField", () => { }); it("should not be copyable when value is empty", () => { - const { container } = render(); - // antd adds a .ant-typography-copy element when copyable; should not be present - expect(container.querySelector(".ant-typography-copy")).not.toBeInTheDocument(); + render(); + expect(screen.queryByRole("button", { name: "Copy User ID" })).not.toBeInTheDocument(); }); it("should not be copyable when value is default_user_id and defaultUserIdCheck is true", () => { - const { container } = render(); - expect(container.querySelector(".ant-typography-copy")).not.toBeInTheDocument(); + render(); + expect(screen.queryByRole("button", { name: "Copy User ID" })).not.toBeInTheDocument(); + }); + + it("should not be copyable when copyable is false", () => { + render(); + expect(screen.queryByRole("button", { name: "Copy User ID" })).not.toBeInTheDocument(); }); it("should be copyable when copyable is true and value is present", () => { - const { container } = render(); - expect(container.querySelector(".ant-typography-copy")).toBeInTheDocument(); + render(); + expect(screen.getByRole("button", { name: "Copy User ID" })).toBeInTheDocument(); }); }); diff --git a/ui/litellm-dashboard/src/components/common_components/LabeledField.tsx b/ui/litellm-dashboard/src/components/common_components/LabeledField.tsx index 75a8236a2fa..6107448babe 100644 --- a/ui/litellm-dashboard/src/components/common_components/LabeledField.tsx +++ b/ui/litellm-dashboard/src/components/common_components/LabeledField.tsx @@ -1,9 +1,8 @@ import React from "react"; -import { Typography, Space } from "antd"; +import CopyButton from "@/components/shared/CopyButton"; +import { cx } from "@/lib/cva.config"; import DefaultProxyAdminTag from "./DefaultProxyAdminTag"; -const { Text } = Typography; - interface LabeledFieldProps { label: string; value: string; @@ -29,24 +28,20 @@ export default function LabeledField({ const valueEl = isDefaultUser ? ( ) : ( - - {displayValue} - + + + {displayValue} + + {isCopyable && } + ); return ( -
- - {icon} - - {label} - - -
{valueEl}
+
+
+ {icon} + {label} +
+
{valueEl}
); } diff --git a/ui/litellm-dashboard/src/components/common_components/MemberTable.tsx b/ui/litellm-dashboard/src/components/common_components/MemberTable.tsx index d58094642d2..3f4cdf5931b 100644 --- a/ui/litellm-dashboard/src/components/common_components/MemberTable.tsx +++ b/ui/litellm-dashboard/src/components/common_components/MemberTable.tsx @@ -1,12 +1,13 @@ +import { Tooltip } from "@/components/atoms/Tooltip"; import { Member } from "@/components/networking"; -import { CrownOutlined, InfoCircleOutlined, UserAddOutlined, UserOutlined } from "@ant-design/icons"; -import { Button, Space, Table, Tag, Tooltip, Typography } from "antd"; +import { StatusBadge } from "@/components/shared/table_cells"; +import { Button } from "@/components/ui/button"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import type { ColumnsType } from "antd/es/table"; +import { Crown, Info, User, UserPlus } from "lucide-react"; import React from "react"; import TableIconActionButton from "./IconActionButton/TableIconActionButtons/TableIconActionButton"; -const { Text } = Typography; - export interface MemberTableProps { members: Member[]; canEdit: boolean; @@ -20,6 +21,21 @@ export interface MemberTableProps { emptyText?: string; } +type ExtraColumn = ColumnsType[number]; + +const extraColumnTitle = (column: ExtraColumn): React.ReactNode => + typeof column.title === "function" ? null : column.title; + +const extraColumnCell = (column: ExtraColumn, member: Member, index: number): React.ReactNode => { + const dataIndex = "dataIndex" in column && typeof column.dataIndex === "string" ? column.dataIndex : undefined; + const value = dataIndex ? member[dataIndex as keyof Member] : undefined; + const rendered = column.render?.(value, member, index); + if (typeof rendered === "string" || typeof rendered === "number") return rendered; + return React.isValidElement(rendered) ? rendered : null; +}; + +const STICKY_ACTIONS_CLASS = "sticky right-0 w-[120px] bg-background"; + export default function MemberTable({ members, canEdit, @@ -32,91 +48,96 @@ export default function MemberTable({ showDeleteForMember, emptyText, }: MemberTableProps) { - const baseColumns: ColumnsType = [ - { - title: "User Email", - dataIndex: "user_email", - key: "user_email", - render: (email: string | null) => {email || "-"}, - }, - { - title: "User ID", - dataIndex: "user_id", - key: "user_id", - render: (userId: string | null) => - userId === "default_user_id" ? Default Proxy Admin : {userId || "-"}, - }, - { - title: roleTooltip ? ( - - {roleColumnTitle} - - - - - ) : ( - roleColumnTitle - ), - dataIndex: "role", - key: "role", - render: (role: string) => ( - - {role?.toLowerCase() === "admin" || role?.toLowerCase() === "org_admin" ? ( - - ) : ( - - )} - {role || "-"} - - ), - }, - ...extraColumns, - { - title: "Actions", - key: "actions", - fixed: "right" as const, - width: 120, - render: (_: unknown, record: Member) => - canEdit ? ( - - onEdit(record)} - /> - {(!showDeleteForMember || showDeleteForMember(record)) && ( - onDelete(record)} - /> - )} - - ) : null, - }, - ]; - return ( - +
{members.length} Member{members.length !== 1 ? "s" : ""} - record.user_id ?? record.user_email ?? JSON.stringify(record)} - pagination={false} - size="small" - scroll={{ x: "max-content" }} - locale={emptyText ? { emptyText } : undefined} - /> +
+ + + User Email + User ID + + {roleTooltip ? ( + + {roleColumnTitle} + + + + + ) : ( + roleColumnTitle + )} + + {extraColumns.map((column, columnIndex) => ( + {extraColumnTitle(column)} + ))} + Actions + + + + {members.length === 0 ? ( + + + {emptyText ?? "No data"} + + + ) : ( + members.map((member, memberIndex) => ( + + {member.user_email || "-"} + + {member.user_id === "default_user_id" ? ( + + ) : ( + member.user_id || "-" + )} + + + + {member.role?.toLowerCase() === "admin" || member.role?.toLowerCase() === "org_admin" ? ( + + ) : ( + + )} + {member.role || "-"} + + + {extraColumns.map((column, columnIndex) => ( + {extraColumnCell(column, member, memberIndex)} + ))} + + {canEdit ? ( + + onEdit(member)} + /> + {(!showDeleteForMember || showDeleteForMember(member)) && ( + onDelete(member)} + /> + )} + + ) : null} + + + )) + )} + +
{onAddMember && canEdit && ( - )} - +
); } diff --git a/ui/litellm-dashboard/src/components/common_components/NewBadge.tsx b/ui/litellm-dashboard/src/components/common_components/NewBadge.tsx index fe2c9d7cf93..0184616803e 100644 --- a/ui/litellm-dashboard/src/components/common_components/NewBadge.tsx +++ b/ui/litellm-dashboard/src/components/common_components/NewBadge.tsx @@ -1,4 +1,4 @@ -import { Badge } from "antd"; +import { Badge } from "@/components/ui/badge"; import { useDisableShowNewBadge } from "@/app/(dashboard)/hooks/useDisableShowNewBadge"; export default function NewBadge({ children, dot = false }: { children?: React.ReactNode; dot?: boolean }) { @@ -8,11 +8,14 @@ export default function NewBadge({ children, dot = false }: { children?: React.R return children ? <>{children} : null; } + const badge = dot ? : New; + return children ? ( - + {children} - + {badge} + ) : ( - + badge ); }