redesign credentials table to use Tremor table layout matching Keys page

This commit is contained in:
Ishaan Jaffer 2026-03-11 21:00:41 -07:00
parent ae9b2f93ef
commit 940e957ebe

View file

@ -9,7 +9,8 @@
import React, { useCallback, useEffect, useState } from "react";
import { Spin, message } from "antd";
import { CheckCircleOutlined, DeleteOutlined, LinkOutlined } from "@ant-design/icons";
import { DeleteOutlined, LinkOutlined } from "@ant-design/icons";
import { Badge, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "@tremor/react";
import {
deleteMCPOAuthUserCredential,
listMCPUserCredentials,
@ -86,98 +87,82 @@ const MCPCredentialsTab: React.FC<Props> = ({ accessToken }) => {
c.alias || c.server_name || c.server_id;
return (
<div style={{ width: "100%" }}>
<div className="w-full">
{/* Header */}
<div style={{ marginBottom: 20 }}>
<h2 style={{ margin: "0 0 4px", fontSize: 18, fontWeight: 600, color: "#111827" }}>
App Credentials
</h2>
<p style={{ margin: 0, fontSize: 13, color: "#6b7280" }}>
<div className="mb-4">
<h2 className="text-base font-semibold text-gray-900 mb-0.5">App Credentials</h2>
<p className="text-sm text-gray-500 m-0">
Your stored OAuth connections used automatically in chat.
</p>
</div>
{loading ? (
<div style={{ display: "flex", justifyContent: "center", padding: "48px 0" }}>
<div className="flex justify-center py-12">
<Spin />
</div>
) : credentials.length === 0 ? (
<div style={{
textAlign: "center", color: "#9ca3af", fontSize: 13,
padding: "48px 12px", border: "1px dashed #e5e7eb", borderRadius: 10,
}}>
<LinkOutlined style={{ fontSize: 28, marginBottom: 12, display: "block", color: "#d1d5db" }} />
<div className="text-center text-gray-400 text-sm py-12 border border-dashed border-gray-200 rounded-lg">
<LinkOutlined className="text-2xl mb-3 block text-gray-300" />
No connections yet.
<br />
Go to <strong>Apps</strong> and click <strong>Connect</strong> to authorize an MCP server.
</div>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{credentials.map((cred) => {
const name = displayName(cred);
const isRevoking = revoking.has(cred.server_id);
const exp = expiryLabel(cred.expires_at);
const connected = relativeTime(cred.connected_at);
const isExpired = exp === "Expired";
<div className="rounded-lg border border-gray-200 overflow-hidden">
<Table>
<TableHead>
<TableRow>
<TableHeaderCell className="text-xs font-medium text-gray-500 py-2 px-4">
App
</TableHeaderCell>
<TableHeaderCell className="text-xs font-medium text-gray-500 py-2 px-4">
Connected
</TableHeaderCell>
<TableHeaderCell className="text-xs font-medium text-gray-500 py-2 px-4">
Status
</TableHeaderCell>
<TableHeaderCell className="text-xs font-medium text-gray-500 py-2 px-4 text-right">
Actions
</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{credentials.map((cred) => {
const name = displayName(cred);
const isRevoking = revoking.has(cred.server_id);
const exp = expiryLabel(cred.expires_at);
const connected = relativeTime(cred.connected_at);
const isExpired = exp === "Expired";
return (
<div
key={cred.server_id}
style={{
display: "flex", alignItems: "center", gap: 12,
padding: "14px 16px", border: "1px solid #e5e7eb",
borderRadius: 10, background: "#fff",
}}
>
{/* Status dot */}
<div style={{ flexShrink: 0 }}>
<CheckCircleOutlined style={{ fontSize: 20, color: isExpired ? "#d1d5db" : "#52c41a" }} />
</div>
{/* Info */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 14, fontWeight: 600, color: "#111827", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{name}
</div>
<div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 2, flexWrap: "wrap" }}>
{connected && (
<span style={{ fontSize: 12, color: "#9ca3af" }}>
Connected {connected}
</span>
)}
<span style={{
fontSize: 11, fontWeight: 600,
color: isExpired ? "#ef4444" : "#16a34a",
background: isExpired ? "#fef2f2" : "#f0fdf4",
borderRadius: 4, padding: "1px 6px",
}}>
{exp}
</span>
</div>
</div>
{/* Revoke */}
<button
onClick={() => handleRevoke(cred.server_id)}
disabled={isRevoking}
title="Revoke connection"
style={{
background: "none", border: "1px solid #e5e7eb",
borderRadius: 6, padding: "4px 8px",
cursor: isRevoking ? "not-allowed" : "pointer",
color: "#9ca3af", display: "flex", alignItems: "center",
opacity: isRevoking ? 0.5 : 1, flexShrink: 0,
}}
>
{isRevoking ? (
<Spin size="small" />
) : (
<DeleteOutlined style={{ fontSize: 14 }} />
)}
</button>
</div>
);
})}
return (
<TableRow key={cred.server_id} className="h-10 hover:bg-gray-50">
<TableCell className="py-2 px-4">
<span className="text-sm font-medium text-gray-900">{name}</span>
</TableCell>
<TableCell className="py-2 px-4">
<span className="text-sm text-gray-500">{connected || "—"}</span>
</TableCell>
<TableCell className="py-2 px-4">
<Badge color={isExpired ? "red" : "green"} size="xs">
{exp}
</Badge>
</TableCell>
<TableCell className="py-2 px-4 text-right">
<button
onClick={() => handleRevoke(cred.server_id)}
disabled={isRevoking}
title="Revoke connection"
className={`inline-flex items-center justify-center rounded-md border border-gray-200 px-2 py-1 text-gray-400 hover:text-red-500 hover:border-red-200 transition-colors ${isRevoking ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`}
style={{ background: "none" }}
>
{isRevoking ? <Spin size="small" /> : <DeleteOutlined className="text-sm" />}
</button>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
)}
</div>