Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Database Viewer</title> | |
| <style> | |
| :root { | |
| --primary-color: #6366f1; | |
| --primary-hover: #4f46e5; | |
| --bg-color: #f3f4f6; | |
| --sidebar-bg: #ffffff; | |
| --text-color: #1f2937; | |
| --border-color: #e5e7eb; | |
| } | |
| body { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | |
| margin: 0; | |
| padding: 0; | |
| background-color: var(--bg-color); | |
| color: var(--text-color); | |
| display: flex; | |
| height: 100vh; | |
| overflow: hidden; | |
| } | |
| /* Sidebar */ | |
| #sidebar { | |
| width: 250px; | |
| background-color: var(--sidebar-bg); | |
| border-right: 1px solid var(--border-color); | |
| display: flex; | |
| flex-direction: column; | |
| padding: 1rem; | |
| overflow-y: auto; | |
| } | |
| #sidebar h2 { | |
| font-size: 1.25rem; | |
| margin-bottom: 1rem; | |
| color: var(--primary-color); | |
| } | |
| .table-list { | |
| list-style: none; | |
| padding: 0; | |
| margin: 0; | |
| } | |
| .table-item { | |
| padding: 0.75rem 1rem; | |
| margin-bottom: 0.5rem; | |
| border-radius: 0.375rem; | |
| cursor: pointer; | |
| transition: background-color 0.2s; | |
| font-weight: 500; | |
| } | |
| .table-item:hover { | |
| background-color: #eff6ff; | |
| color: var(--primary-color); | |
| } | |
| .table-item.active { | |
| background-color: var(--primary-color); | |
| color: white; | |
| } | |
| /* Main Content */ | |
| #main-content { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| padding: 1.5rem; | |
| overflow: hidden; | |
| } | |
| header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 1rem; | |
| } | |
| h1 { | |
| font-size: 1.5rem; | |
| margin: 0; | |
| } | |
| /* Table Container */ | |
| #table-container { | |
| flex: 1; | |
| background: white; | |
| border-radius: 0.5rem; | |
| box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); | |
| overflow: auto; | |
| position: relative; | |
| } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| white-space: nowrap; | |
| } | |
| th, td { | |
| padding: 0.75rem 1rem; | |
| text-align: left; | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| th { | |
| background-color: #f9fafb; | |
| font-weight: 600; | |
| position: sticky; | |
| top: 0; | |
| z-index: 10; | |
| } | |
| tr:hover { | |
| background-color: #f9fafb; | |
| } | |
| /* Pagination */ | |
| #pagination { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-top: 1rem; | |
| padding: 0.5rem; | |
| background: white; | |
| border-radius: 0.5rem; | |
| box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); | |
| } | |
| .btn { | |
| padding: 0.5rem 1rem; | |
| border: 1px solid var(--border-color); | |
| background-color: white; | |
| border-radius: 0.375rem; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .btn:hover:not(:disabled) { | |
| background-color: #f3f4f6; | |
| border-color: #d1d5db; | |
| } | |
| .btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| #page-info { | |
| font-size: 0.875rem; | |
| color: #6b7280; | |
| } | |
| /* Loading & Empty States */ | |
| .loading, .empty { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| height: 100%; | |
| color: #6b7280; | |
| font-size: 1.125rem; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="sidebar"> | |
| <h2>Tables</h2> | |
| <ul id="table-list" class="table-list"> | |
| <!-- Tables will be injected here --> | |
| </ul> | |
| </div> | |
| <div id="main-content"> | |
| <header> | |
| <h1 id="current-table-name">Select a Table</h1> | |
| <div id="pagination-controls" style="display: none;"> | |
| <!-- Pagination controls --> | |
| </div> | |
| </header> | |
| <div id="table-container"> | |
| <div class="empty">Select a table from the sidebar to view data.</div> | |
| </div> | |
| <div id="pagination" style="display: none;"> | |
| <button id="prev-btn" class="btn">Previous</button> | |
| <span id="page-info">Page 1 of 1</span> | |
| <button id="next-btn" class="btn">Next</button> | |
| </div> | |
| </div> | |
| <script> | |
| const API_BASE = '/api/schema'; | |
| let currentTable = null; | |
| let currentPage = 1; | |
| let totalPages = 1; | |
| const perPage = 50; | |
| // DOM Elements | |
| const tableListEl = document.getElementById('table-list'); | |
| const currentTableNameEl = document.getElementById('current-table-name'); | |
| const tableContainerEl = document.getElementById('table-container'); | |
| const paginationEl = document.getElementById('pagination'); | |
| const prevBtn = document.getElementById('prev-btn'); | |
| const nextBtn = document.getElementById('next-btn'); | |
| const pageInfoEl = document.getElementById('page-info'); | |
| // Initialize | |
| async function init() { | |
| try { | |
| const response = await fetch(`${API_BASE}/tables`); | |
| const tables = await response.json(); | |
| renderTableList(tables); | |
| } catch (error) { | |
| console.error('Failed to fetch tables:', error); | |
| tableContainerEl.innerHTML = '<div class="empty">Error loading tables.</div>'; | |
| } | |
| } | |
| function renderTableList(tables) { | |
| tableListEl.innerHTML = tables.map(table => ` | |
| <li class="table-item" onclick="loadTable('${table}')">${table}</li> | |
| `).join(''); | |
| } | |
| async function loadTable(tableName, page = 1) { | |
| currentTable = tableName; | |
| currentPage = page; | |
| // Update UI | |
| document.querySelectorAll('.table-item').forEach(el => { | |
| el.classList.toggle('active', el.textContent === tableName); | |
| }); | |
| currentTableNameEl.textContent = tableName; | |
| tableContainerEl.innerHTML = '<div class="loading">Loading...</div>'; | |
| paginationEl.style.display = 'none'; | |
| try { | |
| const response = await fetch(`${API_BASE}/table/${tableName}?page=${page}&per_page=${perPage}`); | |
| const data = await response.json(); | |
| renderTableData(data); | |
| } catch (error) { | |
| console.error(`Failed to fetch data for ${tableName}:`, error); | |
| tableContainerEl.innerHTML = `<div class="empty">Error loading data for ${tableName}.</div>`; | |
| } | |
| } | |
| function renderTableData(data) { | |
| if (!data.data || data.data.length === 0) { | |
| tableContainerEl.innerHTML = '<div class="empty">No data found in this table.</div>'; | |
| paginationEl.style.display = 'none'; | |
| return; | |
| } | |
| // Calculate pagination | |
| totalPages = Math.ceil(data.total / data.per_page); | |
| updatePaginationUI(data.total); | |
| // Build Table | |
| const columns = data.columns; | |
| const rows = data.data; | |
| let html = '<table><thead><tr>'; | |
| columns.forEach(col => { | |
| html += `<th>${col}</th>`; | |
| }); | |
| html += '</tr></thead><tbody>'; | |
| rows.forEach(row => { | |
| html += '<tr>'; | |
| columns.forEach(col => { | |
| let val = row[col]; | |
| if (val === null) val = '<span style="color: #9ca3af;">null</span>'; | |
| else if (typeof val === 'object') val = JSON.stringify(val); | |
| html += `<td>${val}</td>`; | |
| }); | |
| html += '</tr>'; | |
| }); | |
| html += '</tbody></table>'; | |
| tableContainerEl.innerHTML = html; | |
| } | |
| function updatePaginationUI(totalItems) { | |
| if (totalItems === 0) { | |
| paginationEl.style.display = 'none'; | |
| return; | |
| } | |
| paginationEl.style.display = 'flex'; | |
| pageInfoEl.textContent = `Page ${currentPage} of ${totalPages} (${totalItems} items)`; | |
| prevBtn.disabled = currentPage <= 1; | |
| nextBtn.disabled = currentPage >= totalPages; | |
| } | |
| // Event Listeners | |
| prevBtn.addEventListener('click', () => { | |
| if (currentPage > 1) loadTable(currentTable, currentPage - 1); | |
| }); | |
| nextBtn.addEventListener('click', () => { | |
| if (currentPage < totalPages) loadTable(currentTable, currentPage + 1); | |
| }); | |
| // Start | |
| init(); | |
| </script> | |
| </body> | |
| </html> |