feat(ui): mcp_tools/mcp_server_columns to shadcn

- @tremor Icon + @heroicons Pencil/Trash + antd Tooltip + @ant-design
  CheckOutlined \u2192 shadcn Tooltip + lucide Check/Pencil/Trash2.
- Health status badge: emerald (healthy), red (unhealthy), muted
  (unknown/checking). Public/Internal network badges use emerald/
  orange categorical palette.
- Server ID chip uses blue 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 16:21:45 +00:00
parent 5ff1af0dea
commit 9b86cd428f
No known key found for this signature in database
2 changed files with 140 additions and 74 deletions

View file

@ -61,6 +61,7 @@
"src/components/guardrails/guardrail_table.tsx",
"src/components/prompts/prompt_table.tsx",
"src/components/common_components/ModelAliasManager.tsx",
"src/components/mcp_tools/mcp_server_columns.tsx",
"src/components/permissions/VectorStorePermissions.tsx",
"src/components/permissions/AgentPermissions.tsx",
"src/components/permissions/MCPServerPermissions.tsx",

View file

@ -1,11 +1,15 @@
import { useState } from "react";
import { ColumnDef } from "@tanstack/react-table";
import { MCPServer } from "./types";
import { Icon } from "@tremor/react";
import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline";
import { Check, Pencil, Trash2 } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { getMaskedAndFullUrl } from "./utils";
import { Tooltip } from "antd";
import { CheckOutlined } from "@ant-design/icons";
const HealthStatusBadge: React.FC<{
server: MCPServer;
@ -20,8 +24,8 @@ const HealthStatusBadge: React.FC<{
if (isLoadingHealth || isRechecking) {
return (
<span className="inline-flex items-center gap-1.5 text-xs text-gray-400 px-2 py-0.5 rounded-full bg-gray-50 border border-gray-100">
<span className="h-1.5 w-1.5 rounded-full bg-gray-300 animate-pulse"></span>
<span className="inline-flex items-center gap-1.5 text-xs text-muted-foreground px-2 py-0.5 rounded-full bg-muted border border-border">
<span className="h-1.5 w-1.5 rounded-full bg-muted-foreground animate-pulse"></span>
Checking
</span>
);
@ -30,11 +34,11 @@ const HealthStatusBadge: React.FC<{
const getStatusColor = (status: string) => {
switch (status) {
case "healthy":
return "text-green-700 bg-green-50 border border-green-200";
return "text-emerald-700 bg-emerald-50 border border-emerald-200 dark:text-emerald-300 dark:bg-emerald-950/30 dark:border-emerald-900";
case "unhealthy":
return "text-red-700 bg-red-50 border border-red-200";
return "text-red-700 bg-red-50 border border-red-200 dark:text-red-300 dark:bg-red-950/30 dark:border-red-900";
default:
return "text-gray-600 bg-gray-50 border border-gray-200";
return "text-muted-foreground bg-muted border border-border";
}
};
@ -54,36 +58,58 @@ const HealthStatusBadge: React.FC<{
const tooltipContent = (
<div className="max-w-xs">
<div className="font-semibold mb-1">Health Status: {status}</div>
{lastCheck && <div className="text-xs mb-1">Last Check: {new Date(lastCheck).toLocaleString()}</div>}
{lastCheck && (
<div className="text-xs mb-1">
Last Check: {new Date(lastCheck).toLocaleString()}
</div>
)}
{error && (
<div className="text-xs">
<div className="font-medium text-red-400 mb-1">Error:</div>
<div className="font-medium text-red-300 mb-1">Error:</div>
<div className="break-words">{error}</div>
</div>
)}
{!lastCheck && !error && <div className="text-xs text-gray-400">No health check data available</div>}
{isClickable && <div className="text-xs text-gray-400 mt-1">Click to recheck</div>}
{!lastCheck && !error && (
<div className="text-xs text-muted-foreground">
No health check data available
</div>
)}
{isClickable && (
<div className="text-xs text-muted-foreground mt-1">Click to recheck</div>
)}
</div>
);
return (
<Tooltip title={tooltipContent} placement="top">
<span
className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full ${getStatusColor(status)} ${isClickable ? "cursor-pointer hover:opacity-80" : "cursor-default"}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={isClickable ? () => onRecheck(server.server_id) : undefined}
>
<span>{isHovered && isClickable ? "↻" : getStatusIcon(status)}</span>
{isHovered && isClickable
? "Recheck"
: status.charAt(0).toUpperCase() + status.slice(1)}
</span>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span
className={cn(
"inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full",
getStatusColor(status),
isClickable ? "cursor-pointer hover:opacity-80" : "cursor-default",
)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={isClickable ? () => onRecheck(server.server_id) : undefined}
>
<span>
{isHovered && isClickable ? "↻" : getStatusIcon(status)}
</span>
{isHovered && isClickable
? "Recheck"
: status.charAt(0).toUpperCase() + status.slice(1)}
</span>
</TooltipTrigger>
<TooltipContent>{tooltipContent}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};
export const mcpServerColumns = (
// eslint-disable-next-line @typescript-eslint/no-unused-vars
userRole: string,
onView: (serverId: string) => void,
onEdit: (serverId: string) => void,
@ -100,7 +126,7 @@ export const mcpServerColumns = (
cell: ({ row }) => (
<button
onClick={() => onView(row.original.server_id)}
className="font-mono text-blue-600 bg-blue-50 hover:bg-blue-100 text-xs font-medium px-2 py-0.5 rounded-md border border-blue-200 text-left truncate whitespace-nowrap cursor-pointer max-w-[15ch] transition-colors"
className="font-mono text-blue-600 bg-blue-50 hover:bg-blue-100 dark:bg-blue-950/30 dark:hover:bg-blue-950/60 dark:text-blue-300 text-xs font-medium px-2 py-0.5 rounded-md border border-blue-200 dark:border-blue-900 text-left truncate whitespace-nowrap cursor-pointer max-w-[15ch] transition-colors"
>
{row.original.server_id.slice(0, 7)}...
</button>
@ -116,11 +142,14 @@ export const mcpServerColumns = (
return (
<div className="flex items-center gap-2">
{logoUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={logoUrl}
alt={`${name ?? "MCP"} logo`}
className="h-5 w-5 rounded object-contain flex-shrink-0"
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }}
onError={(e) => {
(e.target as HTMLImageElement).style.display = "none";
}}
/>
) : null}
<span>{name}</span>
@ -139,7 +168,7 @@ export const mcpServerColumns = (
cell: ({ row }) => {
const url = row.original.url;
if (!url) {
return <span className="text-gray-400">—</span>;
return <span className="text-muted-foreground">—</span>;
}
const { maskedUrl } = getMaskedAndFullUrl(url);
return <span className="font-mono text-sm">{maskedUrl}</span>;
@ -152,10 +181,11 @@ export const mcpServerColumns = (
cell: ({ row }) => {
const transport = row.original.transport || "http";
const specPath = row.original.spec_path;
const displayTransport = specPath && transport !== "stdio" ? "OPENAPI" : transport;
const displayTransport =
specPath && transport !== "stdio" ? "OPENAPI" : transport;
const label = displayTransport.toUpperCase();
return (
<span className="inline-flex items-center text-xs font-medium px-2 py-0.5 rounded border bg-gray-50 text-gray-700 border-gray-200">
<span className="inline-flex items-center text-xs font-medium px-2 py-0.5 rounded border bg-muted text-muted-foreground border-border">
{label}
</span>
);
@ -168,7 +198,7 @@ export const mcpServerColumns = (
cell: ({ getValue }) => {
const authType = (getValue() as string) || "none";
return (
<span className="inline-flex items-center text-xs font-medium px-2 py-0.5 rounded border bg-gray-50 text-gray-700 border-gray-200">
<span className="inline-flex items-center text-xs font-medium px-2 py-0.5 rounded border bg-muted text-muted-foreground border-border">
{authType}
</span>
);
@ -195,20 +225,27 @@ export const mcpServerColumns = (
if (typeof groups[0] === "string") {
const joined = groups.join(", ");
return (
<Tooltip title={joined}>
<div className="flex items-center gap-1 max-w-[200px]">
<span className="inline-flex items-center text-xs font-medium px-1.5 py-0.5 rounded bg-gray-100 text-gray-700 border border-gray-200 truncate max-w-[140px]">
{groups[0]}
</span>
{groups.length > 1 && (
<span className="text-xs text-gray-400 font-medium">+{groups.length - 1}</span>
)}
</div>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-1 max-w-[200px]">
<span className="inline-flex items-center text-xs font-medium px-1.5 py-0.5 rounded bg-muted text-foreground border border-border truncate max-w-[140px]">
{groups[0]}
</span>
{groups.length > 1 && (
<span className="text-xs text-muted-foreground font-medium">
+{groups.length - 1}
</span>
)}
</div>
</TooltipTrigger>
<TooltipContent>{joined}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
}
return <span className="text-xs text-gray-400">—</span>;
return <span className="text-xs text-muted-foreground">—</span>;
},
},
{
@ -217,12 +254,12 @@ export const mcpServerColumns = (
cell: ({ row }) => {
const isPublic = row.original.available_on_public_internet;
return isPublic ? (
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-green-50 text-green-700 rounded-full border border-green-200 text-xs font-medium">
<span className="h-1.5 w-1.5 rounded-full bg-green-500"></span>
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-emerald-50 text-emerald-700 dark:bg-emerald-950/30 dark:text-emerald-300 rounded-full border border-emerald-200 dark:border-emerald-900 text-xs font-medium">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500"></span>
Public
</span>
) : (
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-orange-50 text-orange-700 rounded-full border border-orange-200 text-xs font-medium">
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-orange-50 text-orange-700 dark:bg-orange-950/30 dark:text-orange-300 rounded-full border border-orange-200 dark:border-orange-900 text-xs font-medium">
<span className="h-1.5 w-1.5 rounded-full bg-orange-500"></span>
Internal
</span>
@ -236,12 +273,20 @@ export const mcpServerColumns = (
sortingFn: "datetime",
cell: ({ row }) => {
const server = row.original;
if (!server.created_at) return <span className="text-xs text-gray-400">—</span>;
if (!server.created_at)
return <span className="text-xs text-muted-foreground">—</span>;
const date = new Date(server.created_at);
return (
<Tooltip title={date.toLocaleString()}>
<span className="text-xs text-gray-600">{date.toLocaleDateString()}</span>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="text-xs text-muted-foreground">
{date.toLocaleDateString()}
</span>
</TooltipTrigger>
<TooltipContent>{date.toLocaleString()}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
},
},
@ -252,12 +297,20 @@ export const mcpServerColumns = (
sortingFn: "datetime",
cell: ({ row }) => {
const server = row.original;
if (!server.updated_at) return <span className="text-xs text-gray-400">—</span>;
if (!server.updated_at)
return <span className="text-xs text-muted-foreground">—</span>;
const date = new Date(server.updated_at);
return (
<Tooltip title={date.toLocaleString()}>
<span className="text-xs text-gray-600">{date.toLocaleDateString()}</span>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="text-xs text-muted-foreground">
{date.toLocaleDateString()}
</span>
</TooltipTrigger>
<TooltipContent>{date.toLocaleString()}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
},
},
@ -267,17 +320,17 @@ export const mcpServerColumns = (
cell: ({ row }) => {
const server = row.original;
if (!server.is_byok) {
return <span className="text-gray-300 text-xs">—</span>;
return <span className="text-muted-foreground text-xs">—</span>;
}
if (server.has_user_credential) {
return (
<div className="flex items-center gap-2">
<span className="inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full bg-green-50 text-green-700 border border-green-200">
<CheckOutlined style={{ fontSize: 10 }} /> Connected
<span className="inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full bg-emerald-50 text-emerald-700 border border-emerald-200 dark:bg-emerald-950/30 dark:text-emerald-300 dark:border-emerald-900">
<Check className="h-2.5 w-2.5" /> Connected
</span>
{onByokConnect && (
<button
className="text-xs text-gray-400 hover:text-blue-600 transition-colors"
className="text-xs text-muted-foreground hover:text-primary transition-colors"
onClick={() => onByokConnect(server)}
>
Update
@ -301,22 +354,34 @@ export const mcpServerColumns = (
header: "Actions",
cell: ({ row }) => (
<div className="flex items-center gap-1">
<Tooltip title="Edit">
<button
onClick={() => onEdit(row.original.server_id)}
className="p-1.5 rounded-md text-gray-400 hover:text-blue-600 hover:bg-blue-50 transition-colors"
>
<Icon icon={PencilAltIcon} size="sm" />
</button>
</Tooltip>
<Tooltip title="Delete">
<button
onClick={() => onDelete(row.original.server_id)}
className="p-1.5 rounded-md text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors"
>
<Icon icon={TrashIcon} size="sm" />
</button>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => onEdit(row.original.server_id)}
className="p-1.5 rounded-md text-muted-foreground hover:text-primary hover:bg-primary/10 transition-colors"
aria-label="Edit"
>
<Pencil className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent>Edit</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => onDelete(row.original.server_id)}
className="p-1.5 rounded-md text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors"
aria-label="Delete"
>
<Trash2 className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent>Delete</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
),
},