feat(ui): claude_code_plugins/plugin_table to shadcn

- @ant-design CopyOutlined + @heroicons ChevronDown/Up/SwitchVert/
  Trash + @tremor Badge/Button + antd Tooltip \u2192 shadcn Badge/Button/
  Tooltip + lucide ArrowUpDown/ChevronDown/ChevronUp/Copy/Trash2.
- Tremor Table family preserved (phase 1 whitelist).
- BADGE_COLOR_CLASSES map translates helper-returned tremor color
  tokens into categorical Tailwind palette classes. 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 16:06:49 +00:00
parent 70ad22d3cf
commit f01f8dd628
No known key found for this signature in database
2 changed files with 160 additions and 96 deletions

View file

@ -56,6 +56,7 @@
"src/components/Projects/ProjectsPage.tsx",
"src/components/pass_through_settings.tsx",
"src/components/policies/attachment_table.tsx",
"src/components/claude_code_plugins/plugin_table.tsx",
"src/components/permissions/VectorStorePermissions.tsx",
"src/components/permissions/AgentPermissions.tsx",
"src/components/permissions/MCPServerPermissions.tsx",

View file

@ -1,10 +1,3 @@
import { CopyOutlined } from "@ant-design/icons";
import {
ChevronDownIcon,
ChevronUpIcon,
SwitchVerticalIcon,
TrashIcon,
} from "@heroicons/react/outline";
import {
ColumnDef,
flexRender,
@ -13,9 +6,8 @@ import {
SortingState,
useReactTable,
} from "@tanstack/react-table";
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
import {
Badge,
Button,
Table,
TableBody,
TableCell,
@ -23,12 +15,25 @@ import {
TableHeaderCell,
TableRow,
} from "@tremor/react";
import { Tooltip } from "antd";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import {
ArrowUpDown,
ChevronDown,
ChevronUp,
Copy,
Trash2,
} from "lucide-react";
import React, { useState } from "react";
import NotificationsManager from "../molecules/notifications_manager";
import {
getCategoryBadgeColor
} from "./helpers";
import { getCategoryBadgeColor } from "./helpers";
import { Plugin } from "./types";
interface PluginTableProps {
@ -40,10 +45,32 @@ interface PluginTableProps {
onPluginClick: (pluginId: string) => void;
}
// Map tremor-style badge color tokens to Tailwind palette classes.
const BADGE_COLOR_CLASSES: Record<string, string> = {
gray: "bg-muted text-muted-foreground",
green:
"bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300",
red: "bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-300",
blue: "bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300",
indigo: "bg-indigo-100 text-indigo-700 dark:bg-indigo-950 dark:text-indigo-300",
purple: "bg-purple-100 text-purple-700 dark:bg-purple-950 dark:text-purple-300",
orange: "bg-orange-100 text-orange-700 dark:bg-orange-950 dark:text-orange-300",
amber: "bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-300",
yellow: "bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-300",
cyan: "bg-cyan-100 text-cyan-700 dark:bg-cyan-950 dark:text-cyan-300",
pink: "bg-pink-100 text-pink-700 dark:bg-pink-950 dark:text-pink-300",
rose: "bg-rose-100 text-rose-700 dark:bg-rose-950 dark:text-rose-300",
teal: "bg-teal-100 text-teal-700 dark:bg-teal-950 dark:text-teal-300",
};
const badgeClasses = (color: string): string =>
BADGE_COLOR_CLASSES[color] || BADGE_COLOR_CLASSES.gray;
const PluginTable: React.FC<PluginTableProps> = ({
pluginsList,
isLoading,
onDeleteClick,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
accessToken,
isAdmin,
onPluginClick,
@ -72,25 +99,38 @@ const PluginTable: React.FC<PluginTableProps> = ({
const name = plugin.name || "";
return (
<div className="flex items-center gap-2">
<Tooltip title={name}>
<Button
size="xs"
variant="light"
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate min-w-[150px] justify-start"
onClick={() => onPluginClick(plugin.id)}
>
{name}
</Button>
</Tooltip>
<Tooltip title="Copy Plugin ID">
<CopyOutlined
onClick={(e) => {
e.stopPropagation();
copyToClipboard(plugin.id);
}}
className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs"
/>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => onPluginClick(plugin.id)}
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 dark:bg-blue-950/30 dark:hover:bg-blue-950/60 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate min-w-[150px] rounded"
>
{name}
</button>
</TooltipTrigger>
<TooltipContent>{name}</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
copyToClipboard(plugin.id);
}}
className="cursor-pointer text-muted-foreground hover:text-primary"
aria-label="Copy Plugin ID"
>
<Copy className="h-3 w-3" />
</button>
</TooltipTrigger>
<TooltipContent>Copy Plugin ID</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
);
},
@ -98,10 +138,11 @@ const PluginTable: React.FC<PluginTableProps> = ({
{
header: "Version",
accessorKey: "version",
cell: ({ row }) => {
const version = row.original.version || "N/A";
return <span className="text-xs text-gray-600">{version}</span>;
},
cell: ({ row }) => (
<span className="text-xs text-muted-foreground">
{row.original.version || "N/A"}
</span>
),
},
{
header: "Description",
@ -109,11 +150,16 @@ const PluginTable: React.FC<PluginTableProps> = ({
cell: ({ row }) => {
const description = row.original.description || "No description";
return (
<Tooltip title={description}>
<span className="text-xs text-gray-600 block max-w-[300px] truncate">
{description}
</span>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="text-xs text-muted-foreground block max-w-[300px] truncate">
{description}
</span>
</TooltipTrigger>
<TooltipContent>{description}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
},
},
@ -124,14 +170,14 @@ const PluginTable: React.FC<PluginTableProps> = ({
const category = row.original.category;
if (!category) {
return (
<Badge color="gray" className="text-xs font-normal" size="xs">
<Badge className={cn("text-xs font-normal", badgeClasses("gray"))}>
Uncategorized
</Badge>
);
}
const badgeColor = getCategoryBadgeColor(category);
return (
<Badge color={badgeColor} className="text-xs font-normal" size="xs">
<Badge className={cn("text-xs font-normal", badgeClasses(badgeColor))}>
{category}
</Badge>
);
@ -144,9 +190,10 @@ const PluginTable: React.FC<PluginTableProps> = ({
const plugin = row.original;
return (
<Badge
color={plugin.enabled ? "green" : "gray"}
className="text-xs font-normal"
size="xs"
className={cn(
"text-xs font-normal",
badgeClasses(plugin.enabled ? "green" : "gray"),
)}
>
{plugin.enabled ? "Yes" : "No"}
</Badge>
@ -159,50 +206,58 @@ const PluginTable: React.FC<PluginTableProps> = ({
cell: ({ row }) => {
const plugin = row.original;
return (
<Tooltip title={plugin.created_at}>
<span className="text-xs">{formatDate(plugin.created_at)}</span>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="text-xs">{formatDate(plugin.created_at)}</span>
</TooltipTrigger>
<TooltipContent>{plugin.created_at}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
},
},
...(isAdmin
? [
{
header: "Actions",
id: "actions",
enableSorting: false,
cell: ({ row }: any) => {
const plugin = row.original;
return (
<div className="flex items-center gap-1">
<Tooltip title="Delete skill">
<Button
size="xs"
variant="light"
color="red"
onClick={(e) => {
e.stopPropagation();
onDeleteClick(plugin.name, plugin.name);
}}
icon={TrashIcon}
className="text-red-500 hover:text-red-700 hover:bg-red-50"
/>
</Tooltip>
</div>
);
{
header: "Actions",
id: "actions",
enableSorting: false,
cell: ({ row }: { row: { original: Plugin } }) => {
const plugin = row.original;
return (
<div className="flex items-center gap-1">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={(e) => {
e.stopPropagation();
onDeleteClick(plugin.name, plugin.name);
}}
aria-label="Delete skill"
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>Delete skill</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
);
},
},
},
]
]
: []),
];
const table = useReactTable({
data: pluginsList,
columns,
state: {
sorting,
},
state: { sorting },
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
@ -219,10 +274,11 @@ const PluginTable: React.FC<PluginTableProps> = ({
{headerGroup.headers.map((header) => (
<TableHeaderCell
key={header.id}
className={`py-1 h-8 ${header.id === "actions"
? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]"
className={`py-1 h-8 ${
header.id === "actions"
? "sticky right-0 bg-background shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]"
: ""
}`}
}`}
onClick={
header.column.getCanSort()
? header.column.getToggleSortingHandler()
@ -234,23 +290,23 @@ const PluginTable: React.FC<PluginTableProps> = ({
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
header.column.columnDef.header,
header.getContext(),
)}
</div>
{header.column.getCanSort() && (
<div className="w-4">
{header.column.getIsSorted() ? (
{
asc: (
<ChevronUpIcon className="h-4 w-4 text-blue-500" />
<ChevronUp className="h-4 w-4 text-primary" />
),
desc: (
<ChevronDownIcon className="h-4 w-4 text-blue-500" />
<ChevronDown className="h-4 w-4 text-primary" />
),
}[header.column.getIsSorted() as string]
) : (
<SwitchVerticalIcon className="h-4 w-4 text-gray-400" />
<ArrowUpDown className="h-4 w-4 text-muted-foreground" />
)}
</div>
)}
@ -263,8 +319,11 @@ const PluginTable: React.FC<PluginTableProps> = ({
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<TableCell
colSpan={columns.length}
className="h-8 text-center"
>
<div className="text-center text-muted-foreground">
<p>Loading...</p>
</div>
</TableCell>
@ -273,20 +332,21 @@ const PluginTable: React.FC<PluginTableProps> = ({
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
className="h-8 cursor-pointer hover:bg-gray-50"
className="h-8 cursor-pointer hover:bg-muted"
onClick={() => onPluginClick(row.original.id)}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap ${cell.column.id === "actions"
? "sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]"
className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap ${
cell.column.id === "actions"
? "sticky right-0 bg-background shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]"
: ""
}`}
}`}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
cell.getContext(),
)}
</TableCell>
))}
@ -294,8 +354,11 @@ const PluginTable: React.FC<PluginTableProps> = ({
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<TableCell
colSpan={columns.length}
className="h-8 text-center"
>
<div className="text-center text-muted-foreground">
<p>No skills found. Add one to get started.</p>
</div>
</TableCell>