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