feat(ui): view_users/columns to shadcn

- @tremor Badge/Grid/Icon + antd Tooltip/Checkbox + @ant-design Copy/
  @heroicons Pencil/Trash/InformationCircle/Refresh \u2192 shadcn Badge/
  Tooltip/Checkbox + lucide Copy/Info/Pencil/Trash2/RefreshCcw +
  IconTipButton helper.
- Indigo Virtual Keys badge preserved as categorical accent (file
  added to .eslintrc.json overrides).

Gates: TS \u2713 | Build \u2713.

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 15:26:06 +00:00
parent e39bbc6b3f
commit 9516e199da
No known key found for this signature in database
2 changed files with 138 additions and 63 deletions

View file

@ -52,6 +52,7 @@
"src/components/common_components/AutoRotationView.tsx",
"src/components/agents/agent_card.tsx",
"src/components/agents/agent_table.tsx",
"src/components/view_users/columns.tsx",
"src/components/permissions/VectorStorePermissions.tsx",
"src/components/permissions/AgentPermissions.tsx",
"src/components/permissions/MCPServerPermissions.tsx",

View file

@ -1,9 +1,15 @@
import { ColumnDef } from "@tanstack/react-table";
import { Badge, Grid, Icon } from "@tremor/react";
import { Tooltip, Checkbox } from "antd";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { Copy, Info, Pencil, RefreshCcw, Trash2 } from "lucide-react";
import { UserInfo } from "./types";
import { PencilAltIcon, TrashIcon, InformationCircleIcon, RefreshIcon } from "@heroicons/react/outline";
import { CopyOutlined } from "@ant-design/icons";
import { formatNumberWithCommas, copyToClipboard } from "@/utils/dataUtils";
interface SelectionOptions {
@ -15,6 +21,33 @@ interface SelectionOptions {
isIndeterminate: boolean;
}
const IconTipButton: React.FC<{
tooltip: string;
onClick: () => void;
icon: React.ReactNode;
className?: string;
ariaLabel?: string;
}> = ({ tooltip, onClick, icon, className, ariaLabel }) => (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={onClick}
className={cn(
"cursor-pointer text-muted-foreground hover:text-foreground",
className,
)}
aria-label={ariaLabel ?? tooltip}
>
{icon}
</button>
</TooltipTrigger>
<TooltipContent>{tooltip}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
export const columns = (
possibleUIRoles: Record<string, Record<string, string>>,
handleEdit: (user: UserInfo) => void,
@ -23,7 +56,6 @@ export const columns = (
handleUserClick: (userId: string, openInEditMode?: boolean) => void,
selectionOptions?: SelectionOptions,
): ColumnDef<UserInfo>[] => {
// Backend sortable columns: user_id, user_email, created_at, spend, user_alias, user_role
const baseColumns: ColumnDef<UserInfo>[] = [
{
header: "User ID",
@ -31,19 +63,30 @@ export const columns = (
enableSorting: true,
cell: ({ row }) => (
<div className="flex items-center space-x-2">
<Tooltip title={row.original.user_id}>
<span className="text-xs">{row.original.user_id ? `${row.original.user_id.slice(0, 7)}...` : "-"}</span>
</Tooltip>
{row.original.user_id && (
<Tooltip title="Copy User ID">
<CopyOutlined
onClick={(e) => {
e.stopPropagation();
copyToClipboard(row.original.user_id, "User ID copied to clipboard");
}}
className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs"
/>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="text-xs">
{row.original.user_id
? `${row.original.user_id.slice(0, 7)}...`
: "-"}
</span>
</TooltipTrigger>
<TooltipContent>{row.original.user_id}</TooltipContent>
</Tooltip>
</TooltipProvider>
{row.original.user_id && (
<IconTipButton
tooltip="Copy User ID"
onClick={() =>
copyToClipboard(
row.original.user_id,
"User ID copied to clipboard",
)
}
icon={<Copy className="h-3 w-3" />}
className="hover:text-primary"
/>
)}
</div>
),
@ -52,26 +95,38 @@ export const columns = (
header: "Email",
accessorKey: "user_email",
enableSorting: true,
cell: ({ row }) => <span className="text-xs">{row.original.user_email || "-"}</span>,
cell: ({ row }) => (
<span className="text-xs">{row.original.user_email || "-"}</span>
),
},
{
header: "Global Proxy Role",
accessorKey: "user_role",
enableSorting: true,
cell: ({ row }) => <span className="text-xs">{possibleUIRoles?.[row.original.user_role]?.ui_label || "-"}</span>,
cell: ({ row }) => (
<span className="text-xs">
{possibleUIRoles?.[row.original.user_role]?.ui_label || "-"}
</span>
),
},
{
header: "User Alias",
accessorKey: "user_alias",
enableSorting: false,
cell: ({ row }) => <span className="text-xs">{row.original.user_alias || "-"}</span>,
cell: ({ row }) => (
<span className="text-xs">{row.original.user_alias || "-"}</span>
),
},
{
header: "Spend (USD)",
accessorKey: "spend",
enableSorting: true,
cell: ({ row }) => (
<span className="text-xs">{row.original.spend ? formatNumberWithCommas(row.original.spend, 4) : "-"}</span>
<span className="text-xs">
{row.original.spend
? formatNumberWithCommas(row.original.spend, 4)
: "-"}
</span>
),
},
{
@ -79,22 +134,36 @@ export const columns = (
accessorKey: "max_budget",
enableSorting: false,
cell: ({ row }) => (
<span className="text-xs">{row.original.max_budget !== null ? row.original.max_budget : "Unlimited"}</span>
<span className="text-xs">
{row.original.max_budget !== null
? row.original.max_budget
: "Unlimited"}
</span>
),
},
{
header: () => (
<div className="flex items-center gap-2">
<span>SSO ID</span>
<Tooltip title="SSO ID is the ID of the user in the SSO provider. If the user is not using SSO, this will be null.">
<InformationCircleIcon className="w-4 h-4" />
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3.5 w-3.5" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
SSO ID is the ID of the user in the SSO provider. If the user
is not using SSO, this will be null.
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
),
accessorKey: "sso_user_id",
enableSorting: false,
cell: ({ row }) => (
<span className="text-xs">{row.original.sso_user_id !== null ? row.original.sso_user_id : "-"}</span>
<span className="text-xs">
{row.original.sso_user_id !== null ? row.original.sso_user_id : "-"}
</span>
),
},
{
@ -102,17 +171,18 @@ export const columns = (
accessorKey: "key_count",
enableSorting: false,
cell: ({ row }) => (
<Grid numItems={2}>
<div className="grid grid-cols-2">
{row.original.key_count > 0 ? (
<Badge size="xs" color="indigo">
{row.original.key_count} {row.original.key_count === 1 ? "Key" : "Keys"}
<Badge className="text-xs bg-indigo-100 text-indigo-700 dark:bg-indigo-950 dark:text-indigo-300">
{row.original.key_count}{" "}
{row.original.key_count === 1 ? "Key" : "Keys"}
</Badge>
) : (
<Badge size="xs" color="gray">
<Badge className="text-xs bg-muted text-muted-foreground">
No Keys
</Badge>
)}
</Grid>
</div>
),
},
{
@ -121,7 +191,9 @@ export const columns = (
enableSorting: true,
cell: ({ row }) => (
<span className="text-xs">
{row.original.created_at ? new Date(row.original.created_at).toLocaleDateString() : "-"}
{row.original.created_at
? new Date(row.original.created_at).toLocaleDateString()
: "-"}
</span>
),
},
@ -131,7 +203,9 @@ export const columns = (
enableSorting: false,
cell: ({ row }) => (
<span className="text-xs">
{row.original.updated_at ? new Date(row.original.updated_at).toLocaleDateString() : "-"}
{row.original.updated_at
? new Date(row.original.updated_at).toLocaleDateString()
: "-"}
</span>
),
},
@ -141,38 +215,37 @@ export const columns = (
enableSorting: false,
cell: ({ row }) => (
<div className="flex gap-2">
<Tooltip title="Edit user details">
<Icon
icon={PencilAltIcon}
size="sm"
onClick={() => handleUserClick(row.original.user_id, true)}
className="cursor-pointer hover:text-blue-600"
/>
</Tooltip>
<Tooltip title="Delete user">
<Icon
icon={TrashIcon}
size="sm"
onClick={() => handleDelete(row.original)}
className="cursor-pointer hover:text-red-600"
/>
</Tooltip>
<Tooltip title="Reset Password">
<Icon
icon={RefreshIcon}
size="sm"
onClick={() => handleResetPassword(row.original.user_id)}
className="cursor-pointer hover:text-green-600"
/>
</Tooltip>
<IconTipButton
tooltip="Edit user details"
onClick={() => handleUserClick(row.original.user_id, true)}
icon={<Pencil className="h-4 w-4" />}
className="hover:text-primary"
/>
<IconTipButton
tooltip="Delete user"
onClick={() => handleDelete(row.original)}
icon={<Trash2 className="h-4 w-4" />}
className="hover:text-destructive"
/>
<IconTipButton
tooltip="Reset Password"
onClick={() => handleResetPassword(row.original.user_id)}
icon={<RefreshCcw className="h-4 w-4" />}
className="hover:text-emerald-600 dark:hover:text-emerald-400"
/>
</div>
),
},
];
// Add selection column if selection is enabled
if (selectionOptions) {
const { onSelectUser, onSelectAll, isUserSelected, isAllSelected, isIndeterminate } = selectionOptions;
const {
onSelectUser,
onSelectAll,
isUserSelected,
isAllSelected,
isIndeterminate,
} = selectionOptions;
return [
{
@ -180,16 +253,17 @@ export const columns = (
enableSorting: false,
header: () => (
<Checkbox
indeterminate={isIndeterminate}
checked={isAllSelected}
onChange={(e) => onSelectAll(e.target.checked)}
checked={
isIndeterminate ? "indeterminate" : isAllSelected ? true : false
}
onCheckedChange={(c) => onSelectAll(c === true)}
onClick={(e) => e.stopPropagation()}
/>
),
cell: ({ row }) => (
<Checkbox
checked={isUserSelected(row.original)}
onChange={(e) => onSelectUser(row.original, e.target.checked)}
onCheckedChange={(c) => onSelectUser(row.original, c === true)}
onClick={(e) => e.stopPropagation()}
/>
),