mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
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:
parent
e39bbc6b3f
commit
9516e199da
2 changed files with 138 additions and 63 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
/>
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue