/** * Backend Monitor Interface — Web Database Studio */ (function () { 'use strict'; // State let dbStatus = null; let tablesList = []; let tableSchemasCache = {}; let tabs = []; let activeTabId = null; let tabCounter = 1; let queryHistory = []; try { const saved = localStorage.getItem('dash_backend_monitor_history') || localStorage.getItem('dash_sql_studio_history'); if (saved) queryHistory = JSON.parse(saved); } catch (e) { queryHistory = []; } // DOM Elements const elDbStatusPill = document.getElementById('db-status-pill'); const elDbEngineName = document.getElementById('db-engine-name'); const elDbTargetName = document.getElementById('db-target-name'); const elFooterPort = document.getElementById('footer-port'); const elFooterDialect = document.getElementById('footer-dialect'); const elBtnRunHeader = document.getElementById('btn-run-query-header'); const elBtnNewQuery = document.getElementById('btn-new-query'); const elBtnTemplates = document.getElementById('btn-templates-dropdown'); const elTemplatesMenu = document.getElementById('templates-menu'); const elBtnHistory = document.getElementById('btn-history'); const elTableCountBadge = document.getElementById('table-count-badge'); const elBtnRefreshTables = document.getElementById('btn-refresh-tables'); const elTableSearchInput = document.getElementById('table-search-input'); const elBtnClearSearch = document.getElementById('btn-clear-search'); const elTablesTree = document.getElementById('tables-tree'); const elTabStrip = document.getElementById('tab-strip'); const elBtnAddTab = document.getElementById('btn-add-tab'); const elTabViews = document.getElementById('tab-views'); // Modals const elModalAddRow = document.getElementById('modal-add-row'); const elFormAddRow = document.getElementById('form-add-row'); const elAddModalTableName = document.getElementById('add-modal-table-name'); const elAddRowFields = document.getElementById('add-row-fields'); const elModalEditRow = document.getElementById('modal-edit-row'); const elFormEditRow = document.getElementById('form-edit-row'); const elEditModalTableName = document.getElementById('edit-modal-table-name'); const elEditRowFields = document.getElementById('edit-row-fields'); const elModalDeleteRow = document.getElementById('modal-delete-row'); const elDeleteModalTableName = document.getElementById('delete-modal-table-name'); const elDeleteModalSummary = document.getElementById('delete-modal-summary'); const elBtnConfirmDelete = document.getElementById('btn-confirm-delete'); const elModalHistory = document.getElementById('modal-history'); const elHistoryList = document.getElementById('history-list'); const elBtnClearHistory = document.getElementById('btn-clear-history'); const elToastContainer = document.getElementById('toast-container'); // Active modal pending action storage let pendingDelete = null; let pendingEdit = null; let pendingAdd = null; // ========================================================================== // Toast Alerts // ========================================================================== function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; elToastContainer.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translateX(100%)'; toast.style.transition = 'all 0.2s ease'; setTimeout(() => toast.remove(), 250); }, 3200); } // ========================================================================== // API Fetch Helpers // ========================================================================== async function apiGet(endpoint) { const res = await fetch(endpoint); const data = await res.json(); if (!res.ok) throw new Error(data.error || `HTTP error ${res.status}`); return data; } async function apiPost(endpoint, body) { const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || `HTTP error ${res.status}`); return data; } async function apiPut(endpoint, body) { const res = await fetch(endpoint, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || `HTTP error ${res.status}`); return data; } async function apiDelete(endpoint, body) { const res = await fetch(endpoint, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || `HTTP error ${res.status}`); return data; } // ========================================================================== // Database Status & Tables // ========================================================================== async function loadDbStatus() { try { dbStatus = await apiGet('/api/status'); elDbEngineName.textContent = dbStatus.dialect.toUpperCase(); elDbTargetName.textContent = dbStatus.database || dbStatus.server; elFooterPort.textContent = String(dbStatus.port || window.location.port || 4210); elFooterDialect.textContent = dbStatus.dialect; } catch (err) { elDbEngineName.textContent = 'OFFLINE'; elDbTargetName.textContent = err.message; showToast('Failed to connect to database: ' + err.message, 'error'); } } async function loadTables() { elTablesTree.innerHTML = '
Loading tables...
'; try { const data = await apiGet('/api/tables'); tablesList = data.tables || []; elTableCountBadge.textContent = tablesList.length; renderTablesTree(tablesList); } catch (err) { elTablesTree.innerHTML = `
Error loading tables: ${escapeHtml(err.message)}
`; showToast('Error loading tables: ' + err.message, 'error'); } } function renderTablesTree(tables) { const filter = (elTableSearchInput.value || '').trim().toLowerCase(); const filtered = tables.filter((t) => t.name.toLowerCase().includes(filter)); if (filtered.length === 0) { elTablesTree.innerHTML = '
No matching tables
'; return; } elTablesTree.innerHTML = ''; filtered.forEach((t) => { const node = document.createElement('div'); node.className = 'table-node'; node.dataset.tableName = t.name; node.innerHTML = `
${escapeHtml(t.name)} ${t.rowCount.toLocaleString()}
`; // Header click toggles expand or opens table const header = node.querySelector('.table-node-header'); const infoArea = node.querySelector('.table-node-info'); const btnBrowse = node.querySelector('.btn-browse-table'); const btnQuery = node.querySelector('.btn-query-table'); infoArea.addEventListener('click', (e) => { e.stopPropagation(); toggleTableExpand(node, t.name); }); header.addEventListener('dblclick', () => { openTableTab(t.name); }); btnBrowse.addEventListener('click', (e) => { e.stopPropagation(); openTableTab(t.name); }); btnQuery.addEventListener('click', (e) => { e.stopPropagation(); const sql = dbStatus?.dialect === 'mssql' ? `SELECT TOP 50 * FROM [${t.name}];` : `SELECT * FROM [${t.name}] LIMIT 50;`; openQueryTab(sql, `Query: ${t.name}`); }); elTablesTree.appendChild(node); }); } async function toggleTableExpand(node, tableName) { const isExpanded = node.classList.contains('expanded'); if (isExpanded) { node.classList.remove('expanded'); return; } node.classList.add('expanded'); const sublist = node.querySelector('.table-columns-sublist'); if (tableSchemasCache[tableName]) { renderColumnsSublist(sublist, tableSchemasCache[tableName]); return; } sublist.innerHTML = '
Loading columns...
'; try { const data = await apiGet(`/api/tables/${encodeURIComponent(tableName)}/schema`); tableSchemasCache[tableName] = data.columns || []; renderColumnsSublist(sublist, tableSchemasCache[tableName]); } catch (err) { sublist.innerHTML = `
${escapeHtml(err.message)}
`; } } function renderColumnsSublist(container, columns) { if (!columns.length) { container.innerHTML = '
No columns
'; return; } container.innerHTML = columns .map( (c) => `
${ c.isPrimaryKey ? ` ` : '' } ${escapeHtml(c.name)}
${escapeHtml(c.type)}
` ) .join(''); } // ========================================================================== // Tab Management // ========================================================================== function createTab({ type, title, state = {} }) { const tabId = 'tab_' + tabCounter++; const tab = { id: tabId, type, title, state }; tabs.push(tab); renderTabsHeader(); renderTabView(tab); activateTab(tabId); return tab; } function activateTab(tabId) { activeTabId = tabId; document.querySelectorAll('.tab').forEach((el) => { el.classList.toggle('active', el.dataset.tabId === tabId); }); document.querySelectorAll('.tab-view').forEach((el) => { el.classList.toggle('active', el.id === `view_${tabId}`); }); } function closeTab(tabId) { const index = tabs.findIndex((t) => t.id === tabId); if (index === -1) return; tabs.splice(index, 1); const viewEl = document.getElementById(`view_${tabId}`); if (viewEl) viewEl.remove(); if (tabs.length === 0) { openQueryTab(); } else if (activeTabId === tabId) { const nextIndex = Math.max(0, index - 1); activateTab(tabs[nextIndex].id); } renderTabsHeader(); } function renderTabsHeader() { elTabStrip.innerHTML = ''; tabs.forEach((t) => { const tabEl = document.createElement('div'); tabEl.className = `tab ${t.id === activeTabId ? 'active' : ''}`; tabEl.dataset.tabId = t.id; let iconSvg = ''; if (t.type === 'query') { iconSvg = ` `; } else { iconSvg = ` `; } tabEl.innerHTML = ` ${iconSvg} ${escapeHtml(t.title)} `; tabEl.addEventListener('click', (e) => { if (e.target.closest('.tab-close')) return; activateTab(t.id); }); tabEl.querySelector('.tab-close').addEventListener('click', (e) => { e.stopPropagation(); closeTab(t.id); }); elTabStrip.appendChild(tabEl); }); } function renderTabView(tab) { const viewEl = document.createElement('div'); viewEl.className = 'tab-view'; viewEl.id = `view_${tab.id}`; if (tab.type === 'query') { renderQueryTabViewContent(viewEl, tab); } else if (tab.type === 'table') { renderTableBrowserViewContent(viewEl, tab); } elTabViews.appendChild(viewEl); } // ========================================================================== // Query Editor Tab View // ========================================================================== function openQueryTab(initialSql = '', title = null) { const count = tabs.filter((t) => t.type === 'query').length + 1; const tabTitle = title || `Query ${count}`; const defaultSql = initialSql || '-- Write SQL query here\nSELECT 1 as test;'; return createTab({ type: 'query', title: tabTitle, state: { sql: defaultSql, results: null }, }); } function renderQueryTabViewContent(container, tab) { container.className = 'tab-view query-tab-view'; container.innerHTML = `
Shortcuts: Ctrl+Enter (Run)
Status: Ready
Press Execute or Ctrl+Enter to run query
`; const textarea = container.querySelector('.editor-textarea'); const btnRun = container.querySelector('.btn-run-query'); const btnClear = container.querySelector('.btn-clear-sql'); const selectQuickTable = container.querySelector('.select-table-quick'); const resultsBody = container.querySelector('.results-body'); const resultsMeta = container.querySelector('.results-meta'); const btnExportCsv = container.querySelector('.btn-export-csv'); const btnExportJson = container.querySelector('.btn-export-json'); // Keep state in sync textarea.addEventListener('input', () => { tab.state.sql = textarea.value; }); // Indent with tab key textarea.addEventListener('keydown', (e) => { if (e.key === 'Tab') { e.preventDefault(); const start = textarea.selectionStart; const end = textarea.selectionEnd; textarea.value = textarea.value.substring(0, start) + ' ' + textarea.value.substring(end); textarea.selectionStart = textarea.selectionEnd = start + 2; tab.state.sql = textarea.value; } else if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); executeQuery(); } else if (e.key === 'F5') { e.preventDefault(); executeQuery(); } }); btnClear.addEventListener('click', () => { textarea.value = ''; tab.state.sql = ''; textarea.focus(); }); selectQuickTable.addEventListener('change', () => { const val = selectQuickTable.value; if (!val) return; insertAtCursor(textarea, `[${val}] `); tab.state.sql = textarea.value; selectQuickTable.value = ''; }); btnRun.addEventListener('click', executeQuery); async function executeQuery() { const sqlToRun = textarea.value.trim(); if (!sqlToRun) { showToast('Query is empty', 'error'); return; } btnRun.disabled = true; resultsMeta.innerHTML = 'Executing query...'; try { const result = await apiPost('/api/query', { sql: sqlToRun }); tab.state.results = result; // Record to query history addQueryToHistory(sqlToRun); if (!result.success) { resultsMeta.innerHTML = ` Status: Error Duration: ${result.executionTimeMs} ms `; resultsBody.innerHTML = `
${escapeHtml(result.error)}
`; btnExportCsv.style.display = 'none'; btnExportJson.style.display = 'none'; } else { resultsMeta.innerHTML = ` Status: Success Duration: ${result.executionTimeMs} ms Rows: ${(result.rows || []).length} ${ result.rowsAffected !== undefined ? `Affected: ${result.rowsAffected}` : '' } `; if (result.rows && result.rows.length > 0) { btnExportCsv.style.display = 'inline-flex'; btnExportJson.style.display = 'inline-flex'; renderGrid(resultsBody, result.columns, result.rows); } else { btnExportCsv.style.display = 'none'; btnExportJson.style.display = 'none'; resultsBody.innerHTML = `
Query executed successfully (${result.rowsAffected || 0} rows affected)
`; } } } catch (err) { resultsMeta.innerHTML = `Error: ${escapeHtml(err.message)}`; resultsBody.innerHTML = `
${escapeHtml(err.message)}
`; } finally { btnRun.disabled = false; } } btnExportCsv.addEventListener('click', () => { if (tab.state.results?.rows) { exportToCsv('query_results.csv', tab.state.results.columns, tab.state.results.rows); } }); btnExportJson.addEventListener('click', () => { if (tab.state.results?.rows) { exportToJson('query_results.json', tab.state.results.rows); } }); } // ========================================================================== // Table Browser Tab View // ========================================================================== function openTableTab(tableName) { const existing = tabs.find((t) => t.type === 'table' && t.state.tableName === tableName); if (existing) { activateTab(existing.id); return existing; } return createTab({ type: 'table', title: tableName, state: { tableName, page: 1, pageSize: 50, sortBy: '', sortDir: 'ASC', search: '', data: null, }, }); } function renderTableBrowserViewContent(container, tab) { container.className = 'tab-view table-browser-view'; container.innerHTML = `
Show Page 1 of 1 0 rows
Loading table data...
`; const searchInput = container.querySelector('.browser-search-input'); const btnAddRow = container.querySelector('.btn-add-row-modal'); const btnRefresh = container.querySelector('.btn-refresh-browser'); const btnQuery = container.querySelector('.btn-open-query-from-table'); const selectPageSize = container.querySelector('.select-page-size'); const btnPrevPage = container.querySelector('.btn-prev-page'); const btnNextPage = container.querySelector('.btn-next-page'); const pageIndicator = container.querySelector('.page-indicator'); const totalRecordsBadge = container.querySelector('.total-records-badge'); const gridWrapper = container.querySelector('.grid-wrapper'); const btnExportCsv = container.querySelector('.btn-export-table-csv'); const btnExportJson = container.querySelector('.btn-export-table-json'); // Debounced search let searchDebounce = null; searchInput.addEventListener('input', () => { clearTimeout(searchDebounce); searchDebounce = setTimeout(() => { tab.state.search = searchInput.value; tab.state.page = 1; fetchTableData(); }, 350); }); selectPageSize.addEventListener('change', () => { tab.state.pageSize = parseInt(selectPageSize.value, 10); tab.state.page = 1; fetchTableData(); }); btnPrevPage.addEventListener('click', () => { if (tab.state.page > 1) { tab.state.page--; fetchTableData(); } }); btnNextPage.addEventListener('click', () => { if (tab.state.data && tab.state.page < tab.state.data.totalPages) { tab.state.page++; fetchTableData(); } }); btnRefresh.addEventListener('click', () => { fetchTableData(); }); btnQuery.addEventListener('click', () => { const sql = dbStatus?.dialect === 'mssql' ? `SELECT TOP 50 * FROM [${tab.state.tableName}];` : `SELECT * FROM [${tab.state.tableName}] LIMIT 50;`; openQueryTab(sql, `Query: ${tab.state.tableName}`); }); btnAddRow.addEventListener('click', () => { openAddRowModal(tab.state.tableName, tab.state.data?.columns || [], () => { fetchTableData(); }); }); btnExportCsv.addEventListener('click', () => { if (tab.state.data?.rows) { exportToCsv(`${tab.state.tableName}.csv`, tab.state.data.columns.map((c) => c.name), tab.state.data.rows); } }); btnExportJson.addEventListener('click', () => { if (tab.state.data?.rows) { exportToJson(`${tab.state.tableName}.json`, tab.state.data.rows); } }); async function fetchTableData() { gridWrapper.innerHTML = '
Loading table data...
'; try { const queryParams = new URLSearchParams({ page: tab.state.page, pageSize: tab.state.pageSize, sortBy: tab.state.sortBy || '', sortDir: tab.state.sortDir || 'ASC', search: tab.state.search || '', }); const data = await apiGet(`/api/tables/${encodeURIComponent(tab.state.tableName)}/data?${queryParams.toString()}`); tab.state.data = data; pageIndicator.textContent = `Page ${data.page} of ${data.totalPages}`; totalRecordsBadge.textContent = `${data.total.toLocaleString()} rows`; btnPrevPage.disabled = data.page <= 1; btnNextPage.disabled = data.page >= data.totalPages; renderEditableGrid(gridWrapper, data, tab); } catch (err) { gridWrapper.innerHTML = `
${escapeHtml(err.message)}
`; showToast('Error loading table data: ' + err.message, 'error'); } } // Trigger initial fetch fetchTableData(); } // ========================================================================== // Grid Rendering (Editable Table) // ========================================================================== function renderEditableGrid(container, data, tab) { if (!data.rows || data.rows.length === 0) { container.innerHTML = `
No rows found
`; return; } const tableEl = document.createElement('table'); tableEl.className = 'data-table'; // Header const thead = document.createElement('thead'); const trHead = document.createElement('tr'); // Index column const thIndex = document.createElement('th'); thIndex.className = 'th-index'; thIndex.textContent = '#'; trHead.appendChild(thIndex); // Action column const thAction = document.createElement('th'); thAction.className = 'th-action'; thAction.textContent = 'Actions'; trHead.appendChild(thAction); // Columns data.columns.forEach((col) => { const th = document.createElement('th'); const isSorted = tab.state.sortBy === col.name; const sortArrow = isSorted ? (tab.state.sortDir === 'DESC' ? ' ▼' : ' ▲') : ''; th.innerHTML = `
${ col.isPrimaryKey ? ` ` : '' } ${escapeHtml(col.name)}
${sortArrow}
`; th.addEventListener('click', () => { if (tab.state.sortBy === col.name) { tab.state.sortDir = tab.state.sortDir === 'ASC' ? 'DESC' : 'ASC'; } else { tab.state.sortBy = col.name; tab.state.sortDir = 'ASC'; } tab.state.page = 1; // Re-render const parentTab = container.closest('.tab-view'); if (parentTab) { const refreshBtn = parentTab.querySelector('.btn-refresh-browser'); if (refreshBtn) refreshBtn.click(); } }); trHead.appendChild(th); }); thead.appendChild(trHead); tableEl.appendChild(thead); // Body const tbody = document.createElement('tbody'); data.rows.forEach((row, rowIdx) => { const tr = document.createElement('tr'); // Row number const tdIdx = document.createElement('td'); tdIdx.className = 'td-index'; tdIdx.textContent = (data.page - 1) * data.pageSize + rowIdx + 1; tr.appendChild(tdIdx); // Actions const tdAction = document.createElement('td'); tdAction.className = 'td-action'; tdAction.innerHTML = `
`; tdAction.querySelector('.btn-edit-row').addEventListener('click', () => { openEditRowModal(data.tableName, data.columns, row, () => { const refreshBtn = container.closest('.tab-view')?.querySelector('.btn-refresh-browser'); if (refreshBtn) refreshBtn.click(); }); }); tdAction.querySelector('.btn-delete-row').addEventListener('click', () => { openDeleteRowModal(data.tableName, data.columns, row, () => { const refreshBtn = container.closest('.tab-view')?.querySelector('.btn-refresh-browser'); if (refreshBtn) refreshBtn.click(); }); }); tr.appendChild(tdAction); // Cells data.columns.forEach((col) => { const td = document.createElement('td'); const val = row[col.name]; td.innerHTML = formatCellValue(val); td.title = val === null || val === undefined ? 'NULL' : String(val); td.addEventListener('click', () => { copyToClipboard(val === null || val === undefined ? '' : String(val)); }); tr.appendChild(td); }); tbody.appendChild(tr); }); tableEl.appendChild(tbody); container.innerHTML = ''; container.appendChild(tableEl); } // ========================================================================== // Query Results Grid // ========================================================================== function renderGrid(container, columns, rows) { if (!rows.length) { container.innerHTML = '
No rows returned
'; return; } const tableEl = document.createElement('table'); tableEl.className = 'data-table'; const thead = document.createElement('thead'); const trHead = document.createElement('tr'); const thIdx = document.createElement('th'); thIdx.className = 'th-index'; thIdx.textContent = '#'; trHead.appendChild(thIdx); columns.forEach((colName) => { const th = document.createElement('th'); th.textContent = colName; trHead.appendChild(th); }); thead.appendChild(trHead); tableEl.appendChild(thead); const tbody = document.createElement('tbody'); rows.forEach((row, i) => { const tr = document.createElement('tr'); const tdIdx = document.createElement('td'); tdIdx.className = 'td-index'; tdIdx.textContent = i + 1; tr.appendChild(tdIdx); columns.forEach((colName) => { const td = document.createElement('td'); const val = row[colName]; td.innerHTML = formatCellValue(val); td.title = val === null || val === undefined ? 'NULL' : String(val); td.addEventListener('click', () => { copyToClipboard(val === null || val === undefined ? '' : String(val)); }); tr.appendChild(td); }); tbody.appendChild(tr); }); tableEl.appendChild(tbody); container.innerHTML = ''; container.appendChild(tableEl); } function formatCellValue(val) { if (val === null || val === undefined) { return 'NULL'; } if (typeof val === 'boolean') { return `${val ? 'TRUE' : 'FALSE'}`; } return escapeHtml(String(val)); } // ========================================================================== // Modals Implementation // ========================================================================== function openAddRowModal(tableName, columns, onSuccess) { elAddModalTableName.textContent = tableName; elAddRowFields.innerHTML = ''; pendingAdd = { tableName, onSuccess }; columns.forEach((c) => { const isAutoPk = c.isPrimaryKey && (c.type.includes('INT') || c.type.includes('SERIAL')); const group = document.createElement('div'); group.className = 'form-group'; group.innerHTML = ` `; elAddRowFields.appendChild(group); }); elModalAddRow.classList.remove('hidden'); } elFormAddRow.addEventListener('submit', async (e) => { e.preventDefault(); if (!pendingAdd) return; const formData = new FormData(elFormAddRow); const rowObj = {}; for (const [key, val] of formData.entries()) { if (val !== '') { rowObj[key] = val; } } try { await apiPost(`/api/tables/${encodeURIComponent(pendingAdd.tableName)}/row`, { row: rowObj }); showToast('Record inserted successfully', 'success'); elModalAddRow.classList.add('hidden'); if (pendingAdd.onSuccess) pendingAdd.onSuccess(); } catch (err) { showToast('Insert failed: ' + err.message, 'error'); } }); function openEditRowModal(tableName, columns, row, onSuccess) { elEditModalTableName.textContent = tableName; elEditRowFields.innerHTML = ''; pendingEdit = { tableName, row, columns, onSuccess }; const pkCols = columns.filter((c) => c.isPrimaryKey).map((c) => c.name); columns.forEach((c) => { const isPk = c.isPrimaryKey; const currentVal = row[c.name]; const group = document.createElement('div'); group.className = 'form-group'; group.innerHTML = ` `; elEditRowFields.appendChild(group); }); elModalEditRow.classList.remove('hidden'); } elFormEditRow.addEventListener('submit', async (e) => { e.preventDefault(); if (!pendingEdit) return; const { tableName, row, columns, onSuccess } = pendingEdit; const pkCols = columns.filter((c) => c.isPrimaryKey).map((c) => c.name); // Primary key payload const pkObj = {}; if (pkCols.length > 0) { pkCols.forEach((col) => { pkObj[col] = row[col]; }); } else { // Fallback: use all columns columns.forEach((c) => { pkObj[c.name] = row[c.name]; }); } // Changes payload const formData = new FormData(elFormEditRow); const changes = {}; columns.forEach((c) => { if (c.isPrimaryKey) return; const newVal = formData.get(c.name); if (newVal !== (row[c.name] === null ? '' : String(row[c.name]))) { changes[c.name] = newVal === '' ? (c.isNullable ? null : '') : newVal; } }); if (Object.keys(changes).length === 0) { showToast('No fields were changed', 'info'); elModalEditRow.classList.add('hidden'); return; } try { await apiPut(`/api/tables/${encodeURIComponent(tableName)}/row`, { primaryKey: pkObj, changes, }); showToast('Record updated successfully', 'success'); elModalEditRow.classList.add('hidden'); if (onSuccess) onSuccess(); } catch (err) { showToast('Update failed: ' + err.message, 'error'); } }); function openDeleteRowModal(tableName, columns, row, onSuccess) { elDeleteModalTableName.textContent = tableName; pendingDelete = { tableName, row, columns, onSuccess }; const pkCols = columns.filter((c) => c.isPrimaryKey).map((c) => c.name); let summaryText = ''; if (pkCols.length > 0) { summaryText = pkCols.map((c) => `${c} = ${row[c]}`).join(', '); } else { summaryText = Object.entries(row) .slice(0, 4) .map(([k, v]) => `${k}: ${v}`) .join('\n'); } elDeleteModalSummary.textContent = summaryText; elModalDeleteRow.classList.remove('hidden'); } elBtnConfirmDelete.addEventListener('click', async () => { if (!pendingDelete) return; const { tableName, row, columns, onSuccess } = pendingDelete; const pkCols = columns.filter((c) => c.isPrimaryKey).map((c) => c.name); const pkObj = {}; if (pkCols.length > 0) { pkCols.forEach((c) => { pkObj[c] = row[c]; }); } else { columns.forEach((c) => { pkObj[c.name] = row[c.name]; }); } try { await apiDelete(`/api/tables/${encodeURIComponent(tableName)}/row`, { primaryKey: pkObj }); showToast('Record deleted successfully', 'success'); elModalDeleteRow.classList.add('hidden'); if (onSuccess) onSuccess(); } catch (err) { showToast('Delete failed: ' + err.message, 'error'); } }); // Modal dismiss handlers document.querySelectorAll('[data-close-modal]').forEach((btn) => { btn.addEventListener('click', () => { document.querySelectorAll('.modal-overlay').forEach((m) => m.classList.add('hidden')); }); }); window.addEventListener('keydown', (e) => { if (e.key === 'Escape') { document.querySelectorAll('.modal-overlay').forEach((m) => m.classList.add('hidden')); elTemplatesMenu.classList.remove('show'); } }); // ========================================================================== // Query History // ========================================================================== function addQueryToHistory(sql) { if (!sql) return; const item = { query: sql, timestamp: new Date().toISOString() }; queryHistory.unshift(item); if (queryHistory.length > 50) queryHistory.pop(); try { localStorage.setItem('dash_backend_monitor_history', JSON.stringify(queryHistory)); } catch (e) { // ignore } } function openHistoryModal() { elHistoryList.innerHTML = ''; if (queryHistory.length === 0) { elHistoryList.innerHTML = '
No query history yet
'; } else { queryHistory.forEach((h) => { const item = document.createElement('div'); item.className = 'history-item'; item.innerHTML = `
${escapeHtml(h.query)}
${new Date(h.timestamp).toLocaleTimeString()}
`; item.addEventListener('click', () => { openQueryTab(h.query); elModalHistory.classList.add('hidden'); }); elHistoryList.appendChild(item); }); } elModalHistory.classList.remove('hidden'); } elBtnHistory.addEventListener('click', openHistoryModal); elBtnClearHistory.addEventListener('click', () => { queryHistory = []; try { localStorage.removeItem('dash_sql_studio_history'); } catch (e) {} openHistoryModal(); showToast('Query history cleared', 'info'); }); // Snippets Dropdown elBtnTemplates.addEventListener('click', (e) => { e.stopPropagation(); elTemplatesMenu.classList.toggle('show'); }); document.addEventListener('click', () => { elTemplatesMenu.classList.remove('show'); }); document.querySelectorAll('[data-template]').forEach((btn) => { btn.addEventListener('click', () => { const t = btn.dataset.template; let sql = ''; if (t === 'select') sql = 'SELECT * FROM [table] LIMIT 50;'; else if (t === 'count') sql = 'SELECT COUNT(*) as total FROM [table];'; else if (t === 'insert') sql = 'INSERT INTO [table] ([col1], [col2]) VALUES (\'val1\', \'val2\');'; else if (t === 'update') sql = 'UPDATE [table] SET [col1] = \'new_val\' WHERE [id] = 1;'; else if (t === 'delete') sql = 'DELETE FROM [table] WHERE [id] = 1;'; openQueryTab(sql); }); }); // Global Header Run button elBtnRunHeader.addEventListener('click', () => { const activeView = document.querySelector('.tab-view.active'); if (activeView) { const runBtn = activeView.querySelector('.btn-run-query'); if (runBtn) runBtn.click(); else showToast('Active tab is not a query tab', 'info'); } }); elBtnNewQuery.addEventListener('click', () => openQueryTab()); elBtnAddTab.addEventListener('click', () => openQueryTab()); elBtnRefreshTables.addEventListener('click', () => loadTables()); // Search input filter elTableSearchInput.addEventListener('input', () => { const val = elTableSearchInput.value.trim(); elBtnClearSearch.classList.toggle('hidden', val === ''); renderTablesTree(tablesList); }); elBtnClearSearch.addEventListener('click', () => { elTableSearchInput.value = ''; elBtnClearSearch.classList.add('hidden'); renderTablesTree(tablesList); }); // ========================================================================== // Export & Utility Helpers // ========================================================================== function escapeHtml(str) { if (str === null || str === undefined) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function insertAtCursor(textarea, text) { const start = textarea.selectionStart; const end = textarea.selectionEnd; textarea.value = textarea.value.substring(0, start) + text + textarea.value.substring(end); textarea.selectionStart = textarea.selectionEnd = start + text.length; textarea.focus(); } function copyToClipboard(text) { if (!text) return; navigator.clipboard.writeText(text).then( () => showToast('Copied to clipboard', 'info'), () => showToast('Failed to copy', 'error') ); } function exportToCsv(filename, columns, rows) { const lines = []; lines.push(columns.map((c) => `"${c.replace(/"/g, '""')}"`).join(',')); rows.forEach((r) => { lines.push( columns .map((c) => { const v = r[c]; if (v === null || v === undefined) return ''; return `"${String(v).replace(/"/g, '""')}"`; }) .join(',') ); }); const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8;' }); downloadBlob(blob, filename); } function exportToJson(filename, rows) { const blob = new Blob([JSON.stringify(rows, null, 2)], { type: 'application/json' }); downloadBlob(blob, filename); } function downloadBlob(blob, filename) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showToast(`Downloaded ${filename}`, 'success'); } // ========================================================================== // Initialization // ========================================================================== async function init() { await loadDbStatus(); await loadTables(); // Default initial query tab openQueryTab('-- Backend Monitor Interface\nSELECT 1 as connected;'); } init(); })();