Skip to main content
Glama
Database.tsx11.3 kB
import { useEffect, useState } from 'react'; import { Database as DatabaseIcon, Table, ChevronRight, RefreshCw, Search } from 'lucide-react'; import api from '../lib/api'; interface TableInfo { table_name: string; row_count: string; total_size: string; } interface ColumnInfo { column_name: string; data_type: string; is_nullable: string; column_default: string | null; } interface TableData { schema: ColumnInfo[]; data: Record<string, unknown>[]; total: number; } export default function Database() { const [tables, setTables] = useState<TableInfo[]>([]); const [selectedTable, setSelectedTable] = useState<string | null>(null); const [tableData, setTableData] = useState<TableData | null>(null); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [rowsPerPage] = useState(50); useEffect(() => { loadTables(); }, []); useEffect(() => { if (selectedTable) { loadTableData(selectedTable, currentPage); } }, [selectedTable, currentPage]); async function loadTables() { try { setLoading(true); const res = await api.get(`/v1/database/tables`); setTables(res.data.tables || []); setLoading(false); } catch (err) { console.error('Failed to load tables:', err); setLoading(false); } } async function loadTableData(tableName: string, page: number) { try { const offset = (page - 1) * rowsPerPage; const res = await api.get(`/v1/database/table/${tableName}`, { params: { limit: rowsPerPage, offset } }); setTableData(res.data); } catch (err) { console.error('Failed to load table data:', err); } } const filteredTables = tables.filter(t => t.table_name.toLowerCase().includes(searchTerm.toLowerCase()) ); const totalPages = tableData ? Math.ceil(tableData.total / rowsPerPage) : 0; if (loading) { return ( <div className="flex items-center justify-center h-64"> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div> </div> ); } return ( <div className="space-y-6"> {/* Header */} <div className="flex items-center justify-between"> <div className="flex items-center gap-3"> <DatabaseIcon className="w-8 h-8 text-blue-500" /> <div> <h1 className="text-2xl font-bold text-white">Database Management</h1> <p className="text-sm text-slate-400">Browse and view PostgreSQL tables</p> </div> </div> <button onClick={loadTables} className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors" > <RefreshCw className="w-4 h-4" /> Refresh </button> </div> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> {/* Tables List */} <div className="lg:col-span-1"> <div className="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden"> <div className="p-4 border-b border-slate-700"> <div className="relative"> <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" /> <input type="text" placeholder="Search tables..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 bg-slate-900 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500" /> </div> </div> <div className="max-h-[600px] overflow-y-auto"> {filteredTables.map((table) => ( <button key={table.table_name} onClick={() => { setSelectedTable(table.table_name); setCurrentPage(1); }} className={`w-full flex items-center justify-between p-4 border-b border-slate-700 hover:bg-slate-700 transition-colors ${ selectedTable === table.table_name ? 'bg-slate-700' : '' }`} > <div className="flex items-center gap-3"> <Table className="w-4 h-4 text-blue-400" /> <div className="text-left"> <div className="text-white font-medium">{table.table_name}</div> <div className="text-xs text-slate-400"> {table.row_count} rows · {table.total_size} </div> </div> </div> <ChevronRight className="w-4 h-4 text-slate-400" /> </button> ))} {filteredTables.length === 0 && ( <div className="p-8 text-center text-slate-400"> No tables found </div> )} </div> </div> </div> {/* Table Details */} <div className="lg:col-span-2"> {selectedTable && tableData ? ( <div className="space-y-4"> {/* Schema */} <div className="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden"> <div className="p-4 border-b border-slate-700"> <h2 className="text-lg font-semibold text-white">Schema: {selectedTable}</h2> </div> <div className="overflow-x-auto"> <table className="w-full"> <thead className="bg-slate-900"> <tr> <th className="px-4 py-3 text-left text-xs font-medium text-slate-400 uppercase tracking-wider"> Column </th> <th className="px-4 py-3 text-left text-xs font-medium text-slate-400 uppercase tracking-wider"> Type </th> <th className="px-4 py-3 text-left text-xs font-medium text-slate-400 uppercase tracking-wider"> Nullable </th> <th className="px-4 py-3 text-left text-xs font-medium text-slate-400 uppercase tracking-wider"> Default </th> </tr> </thead> <tbody className="divide-y divide-slate-700"> {tableData.schema.map((col) => ( <tr key={col.column_name} className="hover:bg-slate-700/50"> <td className="px-4 py-3 text-sm font-medium text-white"> {col.column_name} </td> <td className="px-4 py-3 text-sm text-blue-400"> {col.data_type} </td> <td className="px-4 py-3 text-sm text-slate-300"> {col.is_nullable} </td> <td className="px-4 py-3 text-sm text-slate-400"> {col.column_default || '-'} </td> </tr> ))} </tbody> </table> </div> </div> {/* Data */} <div className="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden"> <div className="p-4 border-b border-slate-700 flex items-center justify-between"> <h2 className="text-lg font-semibold text-white"> Data ({tableData.total} rows) </h2> {totalPages > 1 && ( <div className="flex items-center gap-2"> <button onClick={() => setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1} className="px-3 py-1 bg-slate-700 text-white rounded disabled:opacity-50 disabled:cursor-not-allowed hover:bg-slate-600" > Previous </button> <span className="text-sm text-slate-400"> Page {currentPage} of {totalPages} </span> <button onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))} disabled={currentPage === totalPages} className="px-3 py-1 bg-slate-700 text-white rounded disabled:opacity-50 disabled:cursor-not-allowed hover:bg-slate-600" > Next </button> </div> )} </div> <div className="overflow-x-auto max-h-[500px] overflow-y-auto"> <table className="w-full"> <thead className="bg-slate-900 sticky top-0"> <tr> {tableData.schema.map((col) => ( <th key={col.column_name} className="px-4 py-3 text-left text-xs font-medium text-slate-400 uppercase tracking-wider whitespace-nowrap" > {col.column_name} </th> ))} </tr> </thead> <tbody className="divide-y divide-slate-700"> {tableData.data.map((row, idx) => ( <tr key={idx} className="hover:bg-slate-700/50"> {tableData.schema.map((col) => ( <td key={col.column_name} className="px-4 py-3 text-sm text-slate-300 whitespace-nowrap" > {typeof row[col.column_name] === 'object' ? JSON.stringify(row[col.column_name]) : String(row[col.column_name] ?? '')} </td> ))} </tr> ))} </tbody> </table> {tableData.data.length === 0 && ( <div className="p-8 text-center text-slate-400"> No data in this table </div> )} </div> </div> </div> ) : ( <div className="bg-slate-800 rounded-lg border border-slate-700 p-12 text-center"> <Table className="w-12 h-12 text-slate-600 mx-auto mb-4" /> <p className="text-slate-400">Select a table to view its data</p> </div> )} </div> </div> </div> ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/babasida246/ai-mcp-gateway'

If you have feedback or need assistance with the MCP directory API, please join our Discord server