mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): MCPServerPermissions to shadcn (expandable list)
- @tremor Text/Badge + @heroicons Server/Chevron + antd Tooltip \u2192 shadcn Badge/Tooltip + lucide Server/ChevronDown/ChevronRight. - Preserves the categorical blue/emerald/purple palette for servers/access-groups/toolsets respectively. File added to .eslintrc.json overrides. - Expand/collapse logic for tool lists under each server/toolset retained; chevron direction tied to expansion state. Gates: TS \u2713 | Build \u2713. Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
1fe796cebe
commit
6b42172e99
2 changed files with 144 additions and 110 deletions
|
|
@ -52,6 +52,7 @@
|
|||
"src/components/agents/agent_card.tsx",
|
||||
"src/components/permissions/VectorStorePermissions.tsx",
|
||||
"src/components/permissions/AgentPermissions.tsx",
|
||||
"src/components/permissions/MCPServerPermissions.tsx",
|
||||
"src/components/DeletedKeysPage/DeletedKeysPage.tsx",
|
||||
"src/components/DeletedTeamsPage/DeletedTeamsPage.tsx"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Text, Badge } from "@tremor/react";
|
||||
import { ServerIcon, ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline";
|
||||
import { Tooltip } from "antd";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { ChevronDown, ChevronRight, Server } from "lucide-react";
|
||||
import { fetchMCPServers, fetchMCPToolsets } from "../networking";
|
||||
import { MCPServer, MCPToolset } from "../mcp_tools/types";
|
||||
|
||||
|
|
@ -18,38 +23,29 @@ export function MCPServerPermissions({
|
|||
mcpAccessGroups = [],
|
||||
mcpToolPermissions = {},
|
||||
mcpToolsets = [],
|
||||
accessToken
|
||||
accessToken,
|
||||
}: MCPServerPermissionsProps) {
|
||||
const [mcpServerDetails, setMCPServerDetails] = useState<MCPServer[]>([]);
|
||||
const [toolsetDetails, setToolsetDetails] = useState<MCPToolset[]>([]);
|
||||
const [expandedServers, setExpandedServers] = useState<Set<string>>(new Set());
|
||||
const [expandedToolsets, setExpandedToolsets] = useState<Set<string>>(new Set());
|
||||
const [expandedServers, setExpandedServers] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
const [expandedToolsets, setExpandedToolsets] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
|
||||
const toggleServerExpansion = (serverId: string) => {
|
||||
setExpandedServers((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(serverId)) {
|
||||
newSet.delete(serverId);
|
||||
} else {
|
||||
newSet.add(serverId);
|
||||
}
|
||||
return newSet;
|
||||
const toggle = (
|
||||
setter: React.Dispatch<React.SetStateAction<Set<string>>>,
|
||||
id: string,
|
||||
) => {
|
||||
setter((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const toggleToolsetExpansion = (toolsetId: string) => {
|
||||
setExpandedToolsets((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(toolsetId)) {
|
||||
newSet.delete(toolsetId);
|
||||
} else {
|
||||
newSet.add(toolsetId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
// Fetch MCP server details when component mounts
|
||||
useEffect(() => {
|
||||
const fetchMCPServerDetails = async () => {
|
||||
if (accessToken && mcpServers.length > 0) {
|
||||
|
|
@ -68,7 +64,6 @@ export function MCPServerPermissions({
|
|||
fetchMCPServerDetails();
|
||||
}, [accessToken, mcpServers.length]);
|
||||
|
||||
// Fetch toolset details
|
||||
useEffect(() => {
|
||||
const fetchToolsets = async () => {
|
||||
if (accessToken && mcpToolsets.length > 0) {
|
||||
|
|
@ -86,17 +81,20 @@ export function MCPServerPermissions({
|
|||
fetchToolsets();
|
||||
}, [accessToken, mcpToolsets.length]);
|
||||
|
||||
// Function to get display name for MCP server
|
||||
const getMCPServerDisplayName = (serverId: string) => {
|
||||
const serverDetail = mcpServerDetails.find((server) => server.server_id === serverId);
|
||||
const serverDetail = mcpServerDetails.find(
|
||||
(s) => s.server_id === serverId,
|
||||
);
|
||||
if (serverDetail) {
|
||||
const truncatedId = serverId.length > 7 ? `${serverId.slice(0, 3)}...${serverId.slice(-4)}` : serverId;
|
||||
const truncatedId =
|
||||
serverId.length > 7
|
||||
? `${serverId.slice(0, 3)}...${serverId.slice(-4)}`
|
||||
: serverId;
|
||||
return `${serverDetail.alias} (${truncatedId})`;
|
||||
}
|
||||
return serverId;
|
||||
};
|
||||
|
||||
// Merge servers and access groups into one list
|
||||
const mergedItems = [
|
||||
...mcpServers.map((server) => ({ type: "server", value: server })),
|
||||
...mcpAccessGroups.map((group) => ({ type: "accessGroup", value: group })),
|
||||
|
|
@ -106,9 +104,9 @@ export function MCPServerPermissions({
|
|||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<ServerIcon className="h-4 w-4 text-blue-600" />
|
||||
<Text className="font-semibold text-gray-900">MCP Servers</Text>
|
||||
<Badge color="blue" size="xs">
|
||||
<Server className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
<span className="font-semibold text-foreground">MCP Servers</span>
|
||||
<Badge className="bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300 text-xs">
|
||||
{totalCount}
|
||||
</Badge>
|
||||
</div>
|
||||
|
|
@ -116,33 +114,51 @@ export function MCPServerPermissions({
|
|||
{totalCount > 0 ? (
|
||||
<div className="max-h-[400px] overflow-y-auto space-y-2 pr-1">
|
||||
{mergedItems.map((item, index) => {
|
||||
const toolsForServer = item.type === "server" ? mcpToolPermissions[item.value] : undefined;
|
||||
const hasToolRestrictions = toolsForServer && toolsForServer.length > 0;
|
||||
const toolsForServer =
|
||||
item.type === "server"
|
||||
? mcpToolPermissions[item.value]
|
||||
: undefined;
|
||||
const hasToolRestrictions =
|
||||
toolsForServer && toolsForServer.length > 0;
|
||||
const isExpanded = expandedServers.has(item.value);
|
||||
|
||||
return (
|
||||
<div key={index} className="space-y-2">
|
||||
<div
|
||||
onClick={() => hasToolRestrictions && toggleServerExpansion(item.value)}
|
||||
className={`flex items-center gap-3 py-2 px-3 rounded-lg border border-gray-200 transition-all ${
|
||||
onClick={() =>
|
||||
hasToolRestrictions &&
|
||||
toggle(setExpandedServers, item.value)
|
||||
}
|
||||
className={`flex items-center gap-3 py-2 px-3 rounded-lg border border-border transition-all ${
|
||||
hasToolRestrictions
|
||||
? 'cursor-pointer hover:bg-gray-50 hover:border-gray-300'
|
||||
: 'bg-white'
|
||||
? "cursor-pointer hover:bg-muted hover:border-foreground/30"
|
||||
: "bg-background"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
{item.type === "server" ? (
|
||||
<Tooltip title={`Full ID: ${item.value}`} placement="top">
|
||||
<div className="inline-flex items-center gap-2 min-w-0">
|
||||
<span className="inline-block w-1.5 h-1.5 bg-blue-500 rounded-full flex-shrink-0"></span>
|
||||
<span className="text-sm font-medium text-gray-900 truncate">{getMCPServerDisplayName(item.value)}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="inline-flex items-center gap-2 min-w-0">
|
||||
<span className="inline-block w-1.5 h-1.5 bg-blue-500 rounded-full flex-shrink-0" />
|
||||
<span className="text-sm font-medium text-foreground truncate">
|
||||
{getMCPServerDisplayName(item.value)}
|
||||
</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Full ID: {item.value}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<div className="inline-flex items-center gap-2 min-w-0">
|
||||
<span className="inline-block w-1.5 h-1.5 bg-green-500 rounded-full flex-shrink-0"></span>
|
||||
<span className="text-sm font-medium text-gray-900 truncate">{item.value}</span>
|
||||
<span className="ml-1 px-1.5 py-0.5 text-[9px] font-semibold text-green-600 bg-green-50 border border-green-200 rounded uppercase tracking-wide flex-shrink-0">
|
||||
<span className="inline-block w-1.5 h-1.5 bg-emerald-500 rounded-full flex-shrink-0" />
|
||||
<span className="text-sm font-medium text-foreground truncate">
|
||||
{item.value}
|
||||
</span>
|
||||
<span className="ml-1 px-1.5 py-0.5 text-[9px] font-semibold text-emerald-600 bg-emerald-50 border border-emerald-200 dark:bg-emerald-950/30 dark:border-emerald-900 dark:text-emerald-300 rounded uppercase tracking-wide flex-shrink-0">
|
||||
Group
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -151,25 +167,28 @@ export function MCPServerPermissions({
|
|||
|
||||
{hasToolRestrictions && (
|
||||
<div className="flex items-center gap-1 flex-shrink-0 whitespace-nowrap">
|
||||
<span className="text-xs font-medium text-gray-600">{toolsForServer.length}</span>
|
||||
<span className="text-xs text-gray-500">{toolsForServer.length === 1 ? "tool" : "tools"}</span>
|
||||
<span className="text-xs font-medium text-foreground">
|
||||
{toolsForServer.length}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{toolsForServer.length === 1 ? "tool" : "tools"}
|
||||
</span>
|
||||
{isExpanded ? (
|
||||
<ChevronDownIcon className="h-3.5 w-3.5 text-gray-400 ml-0.5" />
|
||||
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground ml-0.5" />
|
||||
) : (
|
||||
<ChevronRightIcon className="h-3.5 w-3.5 text-gray-400 ml-0.5" />
|
||||
<ChevronRight className="h-3.5 w-3.5 text-muted-foreground ml-0.5" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Show tool permissions if expanded */}
|
||||
{hasToolRestrictions && isExpanded && (
|
||||
<div className="ml-4 pl-4 border-l-2 border-blue-200 pb-1">
|
||||
<div className="ml-4 pl-4 border-l-2 border-blue-200 dark:border-blue-900 pb-1">
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{toolsForServer.map((tool, toolIndex) => (
|
||||
<span
|
||||
key={toolIndex}
|
||||
className="inline-flex items-center px-2.5 py-1 rounded-lg bg-blue-50 border border-blue-200 text-blue-800 text-xs font-medium"
|
||||
className="inline-flex items-center px-2.5 py-1 rounded-lg bg-blue-50 border border-blue-200 text-blue-800 dark:bg-blue-950/30 dark:border-blue-900 dark:text-blue-200 text-xs font-medium"
|
||||
>
|
||||
{tool}
|
||||
</span>
|
||||
|
|
@ -181,65 +200,79 @@ export function MCPServerPermissions({
|
|||
);
|
||||
})}
|
||||
|
||||
{/* Toolsets section */}
|
||||
{mcpToolsets.length > 0 && mcpToolsets.map((toolsetId, index) => {
|
||||
const detail = toolsetDetails.find((t) => t.toolset_id === toolsetId);
|
||||
const isExpanded = expandedToolsets.has(toolsetId);
|
||||
const toolCount = detail?.tools.length ?? 0;
|
||||
{mcpToolsets.length > 0 &&
|
||||
mcpToolsets.map((toolsetId, index) => {
|
||||
const detail = toolsetDetails.find(
|
||||
(t) => t.toolset_id === toolsetId,
|
||||
);
|
||||
const isExpanded = expandedToolsets.has(toolsetId);
|
||||
const toolCount = detail?.tools.length ?? 0;
|
||||
|
||||
return (
|
||||
<div key={`toolset-${index}`} className="space-y-2">
|
||||
<div
|
||||
onClick={() => toolCount > 0 && toggleToolsetExpansion(toolsetId)}
|
||||
className={`flex items-center gap-3 py-2 px-3 rounded-lg border border-purple-200 transition-all ${
|
||||
toolCount > 0 ? 'cursor-pointer hover:bg-purple-50 hover:border-purple-300' : 'bg-white'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<span className="inline-block w-1.5 h-1.5 bg-purple-500 rounded-full flex-shrink-0"></span>
|
||||
<span className="text-sm font-medium text-gray-900 truncate">
|
||||
{detail?.toolset_name ?? toolsetId}
|
||||
</span>
|
||||
<span className="ml-1 px-1.5 py-0.5 text-[9px] font-semibold text-purple-600 bg-purple-50 border border-purple-200 rounded uppercase tracking-wide flex-shrink-0">
|
||||
Toolset
|
||||
</span>
|
||||
return (
|
||||
<div key={`toolset-${index}`} className="space-y-2">
|
||||
<div
|
||||
onClick={() =>
|
||||
toolCount > 0 && toggle(setExpandedToolsets, toolsetId)
|
||||
}
|
||||
className={`flex items-center gap-3 py-2 px-3 rounded-lg border border-purple-200 dark:border-purple-900 transition-all ${
|
||||
toolCount > 0
|
||||
? "cursor-pointer hover:bg-purple-50 dark:hover:bg-purple-950/30 hover:border-purple-300 dark:hover:border-purple-800"
|
||||
: "bg-background"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<span className="inline-block w-1.5 h-1.5 bg-purple-500 rounded-full flex-shrink-0" />
|
||||
<span className="text-sm font-medium text-foreground truncate">
|
||||
{detail?.toolset_name ?? toolsetId}
|
||||
</span>
|
||||
<span className="ml-1 px-1.5 py-0.5 text-[9px] font-semibold text-purple-600 bg-purple-50 border border-purple-200 dark:bg-purple-950/30 dark:border-purple-900 dark:text-purple-300 rounded uppercase tracking-wide flex-shrink-0">
|
||||
Toolset
|
||||
</span>
|
||||
</div>
|
||||
{toolCount > 0 && (
|
||||
<div className="flex items-center gap-1 flex-shrink-0 whitespace-nowrap">
|
||||
<span className="text-xs font-medium text-foreground">
|
||||
{toolCount}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{toolCount === 1 ? "tool" : "tools"}
|
||||
</span>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground ml-0.5" />
|
||||
) : (
|
||||
<ChevronRight className="h-3.5 w-3.5 text-muted-foreground ml-0.5" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{toolCount > 0 && (
|
||||
<div className="flex items-center gap-1 flex-shrink-0 whitespace-nowrap">
|
||||
<span className="text-xs font-medium text-gray-600">{toolCount}</span>
|
||||
<span className="text-xs text-gray-500">{toolCount === 1 ? "tool" : "tools"}</span>
|
||||
{isExpanded ? (
|
||||
<ChevronDownIcon className="h-3.5 w-3.5 text-gray-400 ml-0.5" />
|
||||
) : (
|
||||
<ChevronRightIcon className="h-3.5 w-3.5 text-gray-400 ml-0.5" />
|
||||
)}
|
||||
|
||||
{toolCount > 0 && isExpanded && detail && (
|
||||
<div className="ml-4 pl-4 border-l-2 border-purple-200 dark:border-purple-900 pb-1">
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{detail.tools.map((tool, toolIndex) => (
|
||||
<span
|
||||
key={toolIndex}
|
||||
className="inline-flex items-center px-2.5 py-1 rounded-lg bg-purple-50 border border-purple-200 text-purple-800 dark:bg-purple-950/30 dark:border-purple-900 dark:text-purple-200 text-xs font-medium"
|
||||
>
|
||||
<span className="text-purple-400 dark:text-purple-500 mr-1 text-[10px]">
|
||||
{tool.server_id.slice(0, 6)}…
|
||||
</span>
|
||||
{tool.tool_name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{toolCount > 0 && isExpanded && detail && (
|
||||
<div className="ml-4 pl-4 border-l-2 border-purple-200 pb-1">
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{detail.tools.map((tool, toolIndex) => (
|
||||
<span
|
||||
key={toolIndex}
|
||||
className="inline-flex items-center px-2.5 py-1 rounded-lg bg-purple-50 border border-purple-200 text-purple-800 text-xs font-medium"
|
||||
>
|
||||
<span className="text-purple-400 mr-1 text-[10px]">{tool.server_id.slice(0, 6)}…</span>
|
||||
{tool.tool_name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-gray-50 border border-gray-200">
|
||||
<ServerIcon className="h-4 w-4 text-gray-400" />
|
||||
<Text className="text-gray-500 text-sm">No MCP servers, access groups, or toolsets configured</Text>
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-muted border border-border">
|
||||
<Server className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground text-sm">
|
||||
No MCP servers, access groups, or toolsets configured
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue