mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
refactor(ui): migrate shared common_components off antd and tremor
Replaces Ant Design and Tremor in the six shared components under src/components/common_components, which between them are reached by nine routes. - antd Table becomes the ui/table primitives, and the Actions column keeps antd's fixed: "right" behaviour via a sticky cell - Tremor Icon, Text and Badge become a plain span, p and StatusBadge - antd Tooltip and Typography copyable become the shadcn Tooltip and the shared CopyButton - every public prop signature is unchanged, since these are shared components and a renamed prop would break callers far from this folder - two tests dropped assertions on antd internal class names and on DOM structure, and gained cases proving a disabled action does not fire onClick MemberTable keeps a type-only import of antd's ColumnsType because a consumer annotates its own column array with it. No antd code ships from the file.
This commit is contained in:
parent
423b791ee0
commit
a98f2380f8
9 changed files with 240 additions and 216 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<AutoRotationViewProps> = ({
|
|||
|
||||
const content = (
|
||||
<div className="space-y-6">
|
||||
{/* Status Section */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<RefreshIcon className="h-4 w-4 text-blue-600" />
|
||||
<Text className="font-semibold text-gray-900">Auto-Rotation</Text>
|
||||
<Badge color={autoRotate ? "green" : "gray"} size="xs">
|
||||
{autoRotate ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
<p className="text-sm font-semibold text-gray-900">Auto-Rotation</p>
|
||||
<StatusBadge tone={autoRotate ? "success" : "neutral"} label={autoRotate ? "Enabled" : "Disabled"} />
|
||||
{autoRotate && rotationInterval && (
|
||||
<>
|
||||
<Text className="text-gray-400">•</Text>
|
||||
<Text className="text-sm text-gray-600">Every {rotationInterval}</Text>
|
||||
<p className="text-sm text-gray-400">•</p>
|
||||
<p className="text-sm text-gray-600">Every {rotationInterval}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rotation History - Show if there's any rotation data OR if auto-rotation is enabled */}
|
||||
{(autoRotate || lastRotationAt || keyRotationAt || nextRotationAt) && (
|
||||
<div className="space-y-3">
|
||||
{/* Last Rotation - Show when available */}
|
||||
{lastRotationAt && (
|
||||
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-200 rounded-md">
|
||||
<ClockIcon className="w-4 h-4 text-gray-500" />
|
||||
<div className="flex items-center gap-2 rounded-md border border-gray-200 bg-gray-50 p-3">
|
||||
<ClockIcon className="h-4 w-4 text-gray-500" />
|
||||
<div className="flex-1">
|
||||
<Text className="font-medium text-gray-700">Last Rotation</Text>
|
||||
<Text className="text-sm text-gray-600">{formatTimestamp(lastRotationAt)}</Text>
|
||||
<p className="text-sm font-medium text-gray-700">Last Rotation</p>
|
||||
<p className="text-sm text-gray-600">{formatTimestamp(lastRotationAt)}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Next Scheduled Rotation - Show when available */}
|
||||
{(keyRotationAt || nextRotationAt) && (
|
||||
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-200 rounded-md">
|
||||
<ClockIcon className="w-4 h-4 text-gray-500" />
|
||||
<div className="flex items-center gap-2 rounded-md border border-gray-200 bg-gray-50 p-3">
|
||||
<ClockIcon className="h-4 w-4 text-gray-500" />
|
||||
<div className="flex-1">
|
||||
<Text className="font-medium text-gray-700">Next Scheduled Rotation</Text>
|
||||
<Text className="text-sm text-gray-600">{formatTimestamp(nextRotationAt || keyRotationAt || "")}</Text>
|
||||
<p className="text-sm font-medium text-gray-700">Next Scheduled Rotation</p>
|
||||
<p className="text-sm text-gray-600">{formatTimestamp(nextRotationAt || keyRotationAt || "")}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No rotation data message - Only show if auto-rotation is enabled but no data */}
|
||||
{autoRotate && !lastRotationAt && !keyRotationAt && !nextRotationAt && (
|
||||
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-100 rounded-md">
|
||||
<ClockIcon className="w-4 h-4 text-gray-500" />
|
||||
<Text className="text-gray-600">No rotation history available</Text>
|
||||
<div className="flex items-center gap-2 rounded-md border border-gray-100 bg-gray-50 p-3">
|
||||
<ClockIcon className="h-4 w-4 text-gray-500" />
|
||||
<p className="text-sm text-gray-600">No rotation history available</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Disabled State - Only show if auto-rotation is disabled AND there's no rotation history */}
|
||||
{!autoRotate && !lastRotationAt && !keyRotationAt && !nextRotationAt && (
|
||||
<div className="flex items-center gap-2 p-3 bg-gray-50 border border-gray-100 rounded-md">
|
||||
<RefreshIcon className="w-4 h-4 text-gray-400" />
|
||||
<Text className="text-gray-600">Auto-rotation is not enabled for this key</Text>
|
||||
<div className="flex items-center gap-2 rounded-md border border-gray-100 bg-gray-50 p-3">
|
||||
<RefreshIcon className="h-4 w-4 text-gray-400" />
|
||||
<p className="text-sm text-gray-600">Auto-rotation is not enabled for this key</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -102,11 +94,11 @@ const AutoRotationView: React.FC<AutoRotationViewProps> = ({
|
|||
|
||||
if (variant === "card") {
|
||||
return (
|
||||
<div className={`bg-white border border-gray-200 rounded-lg p-6 ${className}`}>
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<div className={`rounded-lg border border-gray-200 bg-white p-6 ${className}`}>
|
||||
<div className="mb-6 flex items-center gap-2">
|
||||
<div>
|
||||
<Text className="font-semibold text-gray-900">Auto-Rotation</Text>
|
||||
<Text className="text-xs text-gray-500">Automatic key rotation settings and status for this key</Text>
|
||||
<p className="text-sm font-semibold text-gray-900">Auto-Rotation</p>
|
||||
<p className="text-xs text-gray-500">Automatic key rotation settings and status for this key</p>
|
||||
</div>
|
||||
</div>
|
||||
{content}
|
||||
|
|
@ -116,7 +108,7 @@ const AutoRotationView: React.FC<AutoRotationViewProps> = ({
|
|||
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
<Text className="font-medium text-gray-900 mb-3">Auto-Rotation</Text>
|
||||
<p className="mb-3 text-sm font-medium text-gray-900">Auto-Rotation</p>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
<Icon icon={icon} size="sm" className={"opacity-50 cursor-not-allowed"} data-testid={dataTestId} />
|
||||
) : (
|
||||
<Icon
|
||||
icon={icon}
|
||||
size="sm"
|
||||
onClick={onClick}
|
||||
className={cx("cursor-pointer", className)}
|
||||
<span
|
||||
className="inline-flex shrink-0 cursor-not-allowed items-center justify-center p-1.5 opacity-50"
|
||||
data-testid={dataTestId}
|
||||
/>
|
||||
>
|
||||
<Icon className="size-5 shrink-0" />
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className={cx("inline-flex shrink-0 cursor-pointer items-center justify-center p-1.5", className)}
|
||||
onClick={onClick}
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
<Icon className="size-5 shrink-0" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(<TableIconActionButton variant="Edit" onClick={() => {}} 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(<TableIconActionButton variant="Edit" onClick={onClick} dataTestId="test-button" tooltipText="Edit" />);
|
||||
|
||||
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(
|
||||
<TableIconActionButton variant="Edit" onClick={() => {}} 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(
|
||||
<TableIconActionButton variant="Edit" onClick={() => {}} 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(
|
||||
<TableIconActionButton
|
||||
variant="Edit"
|
||||
onClick={onClick}
|
||||
dataTestId="test-button"
|
||||
disabled
|
||||
tooltipText="Edit"
|
||||
disabledTooltipText="Cannot edit"
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<TableIconActionButton
|
||||
variant="Edit"
|
||||
|
|
@ -55,15 +81,9 @@ describe("TableIconActionButton", () => {
|
|||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
<BaseActionButton icon={icon} onClick={onClick} className={className} disabled={disabled} dataTestId={dataTestId} />
|
||||
);
|
||||
|
||||
if (!title) {
|
||||
return <span>{button}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title={disabled ? disabledTooltipText : tooltipText}>
|
||||
<span>
|
||||
<BaseActionButton
|
||||
icon={icon}
|
||||
onClick={onClick}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
dataTestId={dataTestId}
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<span />}>{button}</TooltipTrigger>
|
||||
<TooltipContent>{title}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,18 +32,22 @@ describe("LabeledField", () => {
|
|||
});
|
||||
|
||||
it("should not be copyable when value is empty", () => {
|
||||
const { container } = render(<LabeledField label="User ID" value="" copyable />);
|
||||
// antd adds a .ant-typography-copy element when copyable; should not be present
|
||||
expect(container.querySelector(".ant-typography-copy")).not.toBeInTheDocument();
|
||||
render(<LabeledField label="User ID" value="" copyable />);
|
||||
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(<LabeledField label="User ID" value="default_user_id" copyable defaultUserIdCheck />);
|
||||
expect(container.querySelector(".ant-typography-copy")).not.toBeInTheDocument();
|
||||
render(<LabeledField label="User ID" value="default_user_id" copyable defaultUserIdCheck />);
|
||||
expect(screen.queryByRole("button", { name: "Copy User ID" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not be copyable when copyable is false", () => {
|
||||
render(<LabeledField label="User ID" value="user-123" />);
|
||||
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(<LabeledField label="User ID" value="user-123" copyable />);
|
||||
expect(container.querySelector(".ant-typography-copy")).toBeInTheDocument();
|
||||
render(<LabeledField label="User ID" value="user-123" copyable />);
|
||||
expect(screen.getByRole("button", { name: "Copy User ID" })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
<DefaultProxyAdminTag userId={value} />
|
||||
) : (
|
||||
<Text
|
||||
strong
|
||||
copyable={isCopyable ? { tooltips: [`Copy ${label}`, "Copied!"] } : false}
|
||||
ellipsis={truncate}
|
||||
style={truncate ? { maxWidth: 160, display: "block" } : undefined}
|
||||
>
|
||||
{displayValue}
|
||||
</Text>
|
||||
<span className="inline-flex min-w-0 items-center gap-1">
|
||||
<strong className={cx("font-semibold", truncate ? "block max-w-40 truncate" : "break-words")}>
|
||||
{displayValue}
|
||||
</strong>
|
||||
{isCopyable && <CopyButton value={value} label={`Copy ${label}`} />}
|
||||
</span>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<Space size={4}>
|
||||
<Text type="secondary">{icon}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12, textTransform: "uppercase", letterSpacing: "0.05em" }}>
|
||||
{label}
|
||||
</Text>
|
||||
</Space>
|
||||
<div>{valueEl}</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-1 text-muted-foreground">
|
||||
{icon}
|
||||
<span className="text-xs tracking-wider uppercase">{label}</span>
|
||||
</div>
|
||||
<div className="min-w-0">{valueEl}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Member>[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<Member> = [
|
||||
{
|
||||
title: "User Email",
|
||||
dataIndex: "user_email",
|
||||
key: "user_email",
|
||||
render: (email: string | null) => <Text>{email || "-"}</Text>,
|
||||
},
|
||||
{
|
||||
title: "User ID",
|
||||
dataIndex: "user_id",
|
||||
key: "user_id",
|
||||
render: (userId: string | null) =>
|
||||
userId === "default_user_id" ? <Tag color="blue">Default Proxy Admin</Tag> : <Text>{userId || "-"}</Text>,
|
||||
},
|
||||
{
|
||||
title: roleTooltip ? (
|
||||
<Space direction="horizontal">
|
||||
{roleColumnTitle}
|
||||
<Tooltip title={roleTooltip}>
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
) : (
|
||||
roleColumnTitle
|
||||
),
|
||||
dataIndex: "role",
|
||||
key: "role",
|
||||
render: (role: string) => (
|
||||
<Space>
|
||||
{role?.toLowerCase() === "admin" || role?.toLowerCase() === "org_admin" ? (
|
||||
<CrownOutlined />
|
||||
) : (
|
||||
<UserOutlined />
|
||||
)}
|
||||
<Text style={{ textTransform: "capitalize" }}>{role || "-"}</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
...extraColumns,
|
||||
{
|
||||
title: "Actions",
|
||||
key: "actions",
|
||||
fixed: "right" as const,
|
||||
width: 120,
|
||||
render: (_: unknown, record: Member) =>
|
||||
canEdit ? (
|
||||
<Space>
|
||||
<TableIconActionButton
|
||||
variant="Edit"
|
||||
tooltipText="Edit member"
|
||||
dataTestId="edit-member"
|
||||
onClick={() => onEdit(record)}
|
||||
/>
|
||||
{(!showDeleteForMember || showDeleteForMember(record)) && (
|
||||
<TableIconActionButton
|
||||
variant="Delete"
|
||||
tooltipText="Delete member"
|
||||
dataTestId="delete-member"
|
||||
onClick={() => onDelete(record)}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
) : null,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<span className="inline-flex text-sm text-gray-700">
|
||||
{members.length} Member{members.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
<Table
|
||||
columns={baseColumns}
|
||||
dataSource={members}
|
||||
rowKey={(record) => record.user_id ?? record.user_email ?? JSON.stringify(record)}
|
||||
pagination={false}
|
||||
size="small"
|
||||
scroll={{ x: "max-content" }}
|
||||
locale={emptyText ? { emptyText } : undefined}
|
||||
/>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>User Email</TableHead>
|
||||
<TableHead>User ID</TableHead>
|
||||
<TableHead>
|
||||
{roleTooltip ? (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
{roleColumnTitle}
|
||||
<Tooltip content={roleTooltip}>
|
||||
<Info className="size-3.5" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
) : (
|
||||
roleColumnTitle
|
||||
)}
|
||||
</TableHead>
|
||||
{extraColumns.map((column, columnIndex) => (
|
||||
<TableHead key={column.key ?? columnIndex}>{extraColumnTitle(column)}</TableHead>
|
||||
))}
|
||||
<TableHead className={STICKY_ACTIONS_CLASS}>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{members.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={extraColumns.length + 4} className="text-center text-muted-foreground">
|
||||
{emptyText ?? "No data"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
members.map((member, memberIndex) => (
|
||||
<TableRow key={member.user_id ?? member.user_email ?? JSON.stringify(member)}>
|
||||
<TableCell>{member.user_email || "-"}</TableCell>
|
||||
<TableCell>
|
||||
{member.user_id === "default_user_id" ? (
|
||||
<StatusBadge tone="info" label="Default Proxy Admin" />
|
||||
) : (
|
||||
member.user_id || "-"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
{member.role?.toLowerCase() === "admin" || member.role?.toLowerCase() === "org_admin" ? (
|
||||
<Crown className="size-3.5" />
|
||||
) : (
|
||||
<User className="size-3.5" />
|
||||
)}
|
||||
<span className="capitalize">{member.role || "-"}</span>
|
||||
</span>
|
||||
</TableCell>
|
||||
{extraColumns.map((column, columnIndex) => (
|
||||
<TableCell key={column.key ?? columnIndex}>{extraColumnCell(column, member, memberIndex)}</TableCell>
|
||||
))}
|
||||
<TableCell className={STICKY_ACTIONS_CLASS}>
|
||||
{canEdit ? (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<TableIconActionButton
|
||||
variant="Edit"
|
||||
tooltipText="Edit member"
|
||||
dataTestId="edit-member"
|
||||
onClick={() => onEdit(member)}
|
||||
/>
|
||||
{(!showDeleteForMember || showDeleteForMember(member)) && (
|
||||
<TableIconActionButton
|
||||
variant="Delete"
|
||||
tooltipText="Delete member"
|
||||
dataTestId="delete-member"
|
||||
onClick={() => onDelete(member)}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{onAddMember && canEdit && (
|
||||
<Button icon={<UserAddOutlined />} type="primary" onClick={onAddMember}>
|
||||
<Button onClick={onAddMember} className="self-start">
|
||||
<UserPlus className="size-4" />
|
||||
Add Member
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ? <Badge className="size-1.5 p-0" /> : <Badge>New</Badge>;
|
||||
|
||||
return children ? (
|
||||
<Badge color="blue" count={dot ? undefined : "New"} dot={dot}>
|
||||
<span className="relative inline-flex">
|
||||
{children}
|
||||
</Badge>
|
||||
<span className="absolute -top-0.5 -right-1">{badge}</span>
|
||||
</span>
|
||||
) : (
|
||||
<Badge color="blue" count={dot ? undefined : "New"} dot={dot} />
|
||||
badge
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue