mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
Polish MCP Servers UI for enterprise-grade look and feel
10 improvements to the MCP Servers table and related components: 1. Remove debug console.logs from mcp_servers.tsx 2. Fix health status icons: distinct ✓/✗/? per state instead of identical dots 3. Health status badges: proper pill styling with rounded-full and borders 4. Health loading state: subtle pulsing dot instead of raw SVG spinner 5. Transport column: color-coded badges (HTTP=blue, SSE=purple, STDIO=amber, OPENAPI=teal) 6. Auth type column: color-coded badges (oauth2=indigo, bearer_token=sky, api_key=emerald) 7. Server ID chip: rounded corners, border, and transition effect 8. Filter bar: lighter border, cleaner labels, vertical divider between filters 9. Network Access: pill badges with colored dots (Public/Internal) 10. Date columns: shorter headers, dash for missing values, tooltip with full datetime Also: - Improved delete modal: cleaner layout, neutral background instead of red - Access Groups column: shows first group with +N count instead of truncated text - Empty state message includes CTA guidance - Updated test to match renamed filter label Co-authored-by: Ishaan Jaff <ishaan-jaff@users.noreply.github.com>
This commit is contained in:
parent
0a2e560e59
commit
35f5d6cc1c
3 changed files with 104 additions and 96 deletions
|
|
@ -21,7 +21,7 @@ export const mcpServerColumns = (
|
|||
cell: ({ row }) => (
|
||||
<button
|
||||
onClick={() => onView(row.original.server_id)}
|
||||
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left w-full truncate whitespace-nowrap cursor-pointer max-w-[15ch]"
|
||||
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"
|
||||
>
|
||||
{row.original.server_id.slice(0, 7)}...
|
||||
</button>
|
||||
|
|
@ -56,16 +56,39 @@ export const mcpServerColumns = (
|
|||
cell: ({ row }) => {
|
||||
const transport = row.original.transport || "http";
|
||||
const specPath = row.original.spec_path;
|
||||
// If server has spec_path, display as "OPENAPI" instead of the raw transport type
|
||||
const displayTransport = specPath && transport !== "stdio" ? "OPENAPI" : transport;
|
||||
return <span>{displayTransport.toUpperCase()}</span>;
|
||||
const label = displayTransport.toUpperCase();
|
||||
const colorClass =
|
||||
label === "HTTP" ? "bg-blue-50 text-blue-700 border-blue-200" :
|
||||
label === "SSE" ? "bg-purple-50 text-purple-700 border-purple-200" :
|
||||
label === "STDIO" ? "bg-amber-50 text-amber-700 border-amber-200" :
|
||||
label === "OPENAPI" ? "bg-teal-50 text-teal-700 border-teal-200" :
|
||||
"bg-gray-50 text-gray-700 border-gray-200";
|
||||
return (
|
||||
<span className={`inline-flex items-center text-xs font-medium px-2 py-0.5 rounded border ${colorClass}`}>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "auth_type",
|
||||
header: "Auth Type",
|
||||
enableSorting: true,
|
||||
cell: ({ getValue }) => <span>{(getValue() as string) || "none"}</span>,
|
||||
cell: ({ getValue }) => {
|
||||
const authType = (getValue() as string) || "none";
|
||||
const colorClass =
|
||||
authType === "oauth2" ? "bg-indigo-50 text-indigo-700 border-indigo-200" :
|
||||
authType === "bearer_token" ? "bg-sky-50 text-sky-700 border-sky-200" :
|
||||
authType === "api_key" ? "bg-emerald-50 text-emerald-700 border-emerald-200" :
|
||||
authType === "basic" ? "bg-orange-50 text-orange-700 border-orange-200" :
|
||||
"bg-gray-50 text-gray-500 border-gray-200";
|
||||
return (
|
||||
<span className={`inline-flex items-center text-xs font-medium px-2 py-0.5 rounded border ${colorClass}`}>
|
||||
{authType}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "health_status",
|
||||
|
|
@ -76,38 +99,34 @@ export const mcpServerColumns = (
|
|||
const lastCheck = server.last_health_check;
|
||||
const error = server.health_check_error;
|
||||
|
||||
// Show loading spinner if health check is in progress
|
||||
if (isLoadingHealth) {
|
||||
return (
|
||||
<div className="flex items-center text-gray-500">
|
||||
<svg className="animate-spin h-4 w-4 mr-1" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span className="text-xs">Loading...</span>
|
||||
</div>
|
||||
<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>
|
||||
Checking
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "healthy":
|
||||
return "text-green-500 bg-green-50 hover:bg-green-100";
|
||||
return "text-green-700 bg-green-50 border border-green-200";
|
||||
case "unhealthy":
|
||||
return "text-red-500 bg-red-50 hover:bg-red-100";
|
||||
return "text-red-700 bg-red-50 border border-red-200";
|
||||
default:
|
||||
return "text-gray-500 bg-gray-50 hover:bg-gray-100";
|
||||
return "text-gray-600 bg-gray-50 border border-gray-200";
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case "healthy":
|
||||
return "●";
|
||||
return "✓";
|
||||
case "unhealthy":
|
||||
return "●";
|
||||
return "✗";
|
||||
default:
|
||||
return "●";
|
||||
return "?";
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -127,12 +146,12 @@ export const mcpServerColumns = (
|
|||
|
||||
return (
|
||||
<Tooltip title={tooltipContent} placement="top">
|
||||
<button
|
||||
className={`font-mono text-xs font-normal px-2 py-0.5 text-left w-full truncate whitespace-nowrap cursor-pointer max-w-[10ch] ${getStatusColor(status)}`}
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full cursor-default ${getStatusColor(status)}`}
|
||||
>
|
||||
<span className="mr-1">{getStatusIcon(status)}</span>
|
||||
<span>{getStatusIcon(status)}</span>
|
||||
{status.charAt(0).toUpperCase() + status.slice(1)}
|
||||
</button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
|
|
@ -143,19 +162,23 @@ export const mcpServerColumns = (
|
|||
cell: ({ row }) => {
|
||||
const groups = row.original.mcp_access_groups;
|
||||
if (Array.isArray(groups) && groups.length > 0) {
|
||||
// If string array
|
||||
if (typeof groups[0] === "string") {
|
||||
const joined = groups.join(", ");
|
||||
return (
|
||||
<Tooltip title={joined}>
|
||||
<span className="max-w-[200px] truncate block">
|
||||
{joined.length > 30 ? `${joined.slice(0, 30)}...` : joined}
|
||||
</span>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
return <span className="text-gray-400 italic">None</span>;
|
||||
return <span className="text-xs text-gray-400">—</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -164,33 +187,47 @@ export const mcpServerColumns = (
|
|||
cell: ({ row }) => {
|
||||
const isPublic = row.original.available_on_public_internet;
|
||||
return isPublic ? (
|
||||
<span className="px-2 py-0.5 bg-green-50 text-green-700 rounded text-xs font-medium">All networks</span>
|
||||
<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>
|
||||
Public
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 py-0.5 bg-orange-50 text-orange-700 rounded text-xs font-medium">Internal only</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="h-1.5 w-1.5 rounded-full bg-orange-500"></span>
|
||||
Internal
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Created At",
|
||||
header: "Created",
|
||||
accessorKey: "created_at",
|
||||
enableSorting: true,
|
||||
sortingFn: "datetime",
|
||||
cell: ({ row }) => {
|
||||
const server = row.original;
|
||||
if (!server.created_at) return <span className="text-xs text-gray-400">—</span>;
|
||||
const date = new Date(server.created_at);
|
||||
return (
|
||||
<span className="text-xs">{server.created_at ? new Date(server.created_at).toLocaleDateString() : "-"}</span>
|
||||
<Tooltip title={date.toLocaleString()}>
|
||||
<span className="text-xs text-gray-600">{date.toLocaleDateString()}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Updated At",
|
||||
header: "Updated",
|
||||
accessorKey: "updated_at",
|
||||
enableSorting: true,
|
||||
sortingFn: "datetime",
|
||||
cell: ({ row }) => {
|
||||
const server = row.original;
|
||||
if (!server.updated_at) return <span className="text-xs text-gray-400">—</span>;
|
||||
const date = new Date(server.updated_at);
|
||||
return (
|
||||
<span className="text-xs">{server.updated_at ? new Date(server.updated_at).toLocaleDateString() : "-"}</span>
|
||||
<Tooltip title={date.toLocaleString()}>
|
||||
<span className="text-xs text-gray-600">{date.toLocaleDateString()}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -305,8 +305,8 @@ describe("MCPServers", () => {
|
|||
expect(screen.getByText("Team B Server")).toBeInTheDocument();
|
||||
expect(screen.getByText("Team A Server 2")).toBeInTheDocument();
|
||||
|
||||
// Find the team select dropdown by looking for the "Current Team:" label
|
||||
const teamLabel = screen.getByText("Current Team:");
|
||||
// Find the team select dropdown by looking for the "Team" label
|
||||
const teamLabel = screen.getByText("Team");
|
||||
const teamSelectContainer = teamLabel.closest("div")?.querySelector(".ant-select");
|
||||
expect(teamSelectContainer).toBeTruthy();
|
||||
|
||||
|
|
|
|||
|
|
@ -48,17 +48,6 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
|
|||
});
|
||||
}, [mcpServers, healthStatuses]);
|
||||
|
||||
// Log allowed_tools from fetched servers
|
||||
React.useEffect(() => {
|
||||
if (mcpServers) {
|
||||
console.log("MCP Servers fetched:", mcpServers);
|
||||
mcpServers.forEach((server) => {
|
||||
console.log(`Server: ${server.server_name || server.server_id}`);
|
||||
console.log(` allowed_tools:`, server.allowed_tools);
|
||||
});
|
||||
}
|
||||
}, [mcpServers]);
|
||||
|
||||
// state
|
||||
const [serverIdToDelete, setServerToDelete] = useState<string | null>(null);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
|
|
@ -257,34 +246,28 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
|
|||
confirmLoading={isDeletingServer}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<AntdText>Are you sure you want to delete this MCP Server? This action cannot be undone.</AntdText>
|
||||
<AntdText className="text-gray-600">This action is permanent and cannot be undone. All associated configurations will be removed.</AntdText>
|
||||
|
||||
{serverToDelete && (
|
||||
<div className="mt-4 p-4 bg-red-50 rounded-lg border border-red-200">
|
||||
<AntdTitle level={5} className="mb-3 text-gray-900">
|
||||
Server Information
|
||||
</AntdTitle>
|
||||
<Descriptions column={1} size="small">
|
||||
<div className="mt-3 p-4 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<Descriptions column={1} size="small" colon={false}>
|
||||
{serverToDelete.server_name && (
|
||||
<Descriptions.Item label={<span className="font-semibold text-gray-700">Server Name</span>}>
|
||||
<AntdText className="text-sm">{serverToDelete.server_name}</AntdText>
|
||||
<Descriptions.Item label={<span className="text-gray-500 text-sm">Name</span>}>
|
||||
<AntdText strong className="text-sm">{serverToDelete.server_name}</AntdText>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
{serverToDelete.alias && (
|
||||
<Descriptions.Item label={<span className="font-semibold text-gray-700">Alias</span>}>
|
||||
<AntdText className="text-sm">{serverToDelete.alias}</AntdText>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
<Descriptions.Item label={<span className="font-semibold text-gray-700">Server ID</span>}>
|
||||
<AntdText code className="text-sm">
|
||||
<Descriptions.Item label={<span className="text-gray-500 text-sm">ID</span>}>
|
||||
<AntdText code className="text-xs">
|
||||
{serverToDelete.server_id}
|
||||
</AntdText>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={<span className="font-semibold text-gray-700">URL</span>}>
|
||||
<AntdText code className="text-sm">
|
||||
{serverToDelete.url}
|
||||
</AntdText>
|
||||
</Descriptions.Item>
|
||||
{serverToDelete.url && (
|
||||
<Descriptions.Item label={<span className="text-gray-500 text-sm">URL</span>}>
|
||||
<AntdText code className="text-xs break-all">
|
||||
{serverToDelete.url}
|
||||
</AntdText>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -353,50 +336,38 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
|
|||
<div className="w-full h-full">
|
||||
<div className="w-full">
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex items-center justify-between bg-gray-50 rounded-lg p-4 border-2 border-gray-200">
|
||||
<div className="flex items-center gap-4">
|
||||
<Text className="text-lg font-semibold text-gray-900">Current Team:</Text>
|
||||
<Select value={selectedTeam} onChange={handleTeamChange} style={{ width: 300 }}>
|
||||
<div className="flex items-center gap-6 bg-white rounded-lg px-4 py-3 border border-gray-200">
|
||||
<div className="flex items-center gap-2">
|
||||
<Text className="text-sm font-medium text-gray-600 whitespace-nowrap">Team</Text>
|
||||
<Select value={selectedTeam} onChange={handleTeamChange} style={{ width: 220 }} size="middle">
|
||||
<Option value="all">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||
<span className="font-medium">{isInternalUser ? "All Available Servers" : "All Servers"}</span>
|
||||
</div>
|
||||
<span className="font-medium">{isInternalUser ? "All Available Servers" : "All Servers"}</span>
|
||||
</Option>
|
||||
<Option value="personal">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span className="font-medium">Personal</span>
|
||||
</div>
|
||||
<span className="font-medium">Personal</span>
|
||||
</Option>
|
||||
{uniqueTeams.map((team) => (
|
||||
<Option key={team.team_id} value={team.team_id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span className="font-medium">{team.team_alias || team.team_id}</span>
|
||||
</div>
|
||||
<span className="font-medium">{team.team_alias || team.team_id}</span>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Text className="text-lg font-semibold text-gray-900 ml-6">
|
||||
Access Group:
|
||||
</div>
|
||||
<div className="h-6 w-px bg-gray-200"></div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Text className="text-sm font-medium text-gray-600 whitespace-nowrap">
|
||||
Access Group
|
||||
<Tooltip title="An MCP Access Group is a set of users or teams that have permission to access specific MCP servers. Use access groups to control and organize who can connect to which servers.">
|
||||
<QuestionCircleOutlined style={{ marginLeft: 4, color: "#888" }} />
|
||||
<QuestionCircleOutlined style={{ marginLeft: 4, color: "#9ca3af" }} />
|
||||
</Tooltip>
|
||||
</Text>
|
||||
<Select value={selectedMcpAccessGroup} onChange={handleMcpAccessGroupChange} style={{ width: 300 }}>
|
||||
<Select value={selectedMcpAccessGroup} onChange={handleMcpAccessGroupChange} style={{ width: 220 }} size="middle">
|
||||
<Option value="all">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||
<span className="font-medium">All Access Groups</span>
|
||||
</div>
|
||||
<span className="font-medium">All Access Groups</span>
|
||||
</Option>
|
||||
{uniqueMcpAccessGroups.map((group) => (
|
||||
<Option key={group} value={group}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span className="font-medium">{group}</span>
|
||||
</div>
|
||||
<span className="font-medium">{group}</span>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
|
|
@ -411,8 +382,8 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
|
|||
renderSubComponent={() => <div></div>}
|
||||
getRowCanExpand={() => false}
|
||||
isLoading={isLoadingServers}
|
||||
noDataMessage="No MCP servers configured"
|
||||
loadingMessage="🚅 Loading MCP servers..."
|
||||
noDataMessage="No MCP servers configured. Click '+ Add New MCP Server' to get started."
|
||||
loadingMessage="Loading MCP servers..."
|
||||
enableSorting={true}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue