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:
Cursor Agent 2026-03-07 06:58:29 +00:00
parent 0a2e560e59
commit 35f5d6cc1c
3 changed files with 104 additions and 96 deletions

View file

@ -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>
);
},
},

View file

@ -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();

View file

@ -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>