const DBManager = { baseUrl: '', init(baseUrl) { this.baseUrl = baseUrl; this.initTree(); this.initConsole(); this.initSqlHighlighting(); this.initAjaxForms(); }, initTree() { document.querySelectorAll('.tree-header').forEach(header => { // Skip if already initialized if (header.dataset.treeInit) return; header.dataset.treeInit = 'true'; header.addEventListener('click', (e) => { const item = header.closest('.tree-item'); const children = item.querySelector('.tree-children'); const href = header.dataset.href; const toggle = e.target.closest('.tree-toggle'); // If clicking on toggle icon, just expand/collapse (don't navigate) if (toggle) { e.preventDefault(); e.stopPropagation(); // Lazy-load tables for databases if needed if (item.dataset.db && children && !children.dataset.loaded) { this.loadTables(item.dataset.db, children); } // Lazy-load columns for tables if needed else if (item.dataset.table && children && !children.dataset.loaded) { // Find the parent database name const dbItem = item.closest('.tree-item[data-db]'); if (dbItem) { this.loadColumns(dbItem.dataset.db, item.dataset.table, children); } } this.toggleTreeItem(item); return; } // If it's a database or table item with href, navigate if (href) { // For databases, also expand and load tables if (item.dataset.db && children && !children.dataset.loaded) { this.loadTables(item.dataset.db, children); } window.location.href = href; return; } }); }); }, toggleTreeItem(item) { item.classList.toggle('expanded'); }, async loadTables(dbName, container) { container.innerHTML = '
Loading...
'; container.dataset.loaded = 'true'; try { const response = await fetch(`${this.baseUrl}database/getStructure/${encodeURIComponent(dbName)}`); const data = await response.json(); if (data.success && data.structure) { let html = ''; for (const [table, columns] of Object.entries(data.structure)) { html += this.renderTableTreeItem(dbName, table, columns); } container.innerHTML = html || '
No tables
'; this.initTree(); this.refreshIcons(); } } catch (error) { container.innerHTML = '
Failed to load
'; } }, async loadColumns(dbName, tableName, container) { container.innerHTML = '
Loading...
'; container.dataset.loaded = 'true'; try { const response = await fetch(`${this.baseUrl}database/getColumns/${encodeURIComponent(dbName)}/${encodeURIComponent(tableName)}`); const data = await response.json(); if (data.success && data.columns) { let html = ''; data.columns.forEach(col => { html += `
${col.Key === 'PRI' ? this.icons.key : this.icons.column} ${this.escapeHtml(col.Field)} ${this.escapeHtml(col.Type)}
`; }); container.innerHTML = html || '
No columns
'; this.refreshIcons(); } } catch (error) { container.innerHTML = '
Failed to load
'; } }, renderTableTreeItem(dbName, tableName, columns) { const columnsHtml = columns.map(col => `
${col.Key === 'PRI' ? this.icons.key : this.icons.column} ${this.escapeHtml(col.Field)} ${this.escapeHtml(col.Type)}
`).join(''); return `
${this.icons.chevron} ${this.icons.table} ${this.escapeHtml(tableName)}
${columnsHtml}
`; }, initConsole() { const consoleHeader = document.querySelector('.dbm-console-header'); if (consoleHeader) { consoleHeader.addEventListener('click', () => { const console = consoleHeader.closest('.dbm-console'); console.classList.toggle('expanded'); localStorage.setItem('dbm-console-expanded', console.classList.contains('expanded')); }); const wasExpanded = localStorage.getItem('dbm-console-expanded') === 'true'; if (wasExpanded) { consoleHeader.closest('.dbm-console').classList.add('expanded'); } } const sqlForm = document.getElementById('sql-form'); if (sqlForm) { sqlForm.addEventListener('submit', async (e) => { e.preventDefault(); await this.executeQuery(sqlForm); }); } }, async executeQuery(form) { const formData = new FormData(form); const resultContainer = document.getElementById('sql-result'); resultContainer.innerHTML = '
'; try { const response = await fetch(`${this.baseUrl}sql/execute`, { method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' } }); const data = await response.json(); this.renderSqlResult(data, resultContainer); } catch (error) { resultContainer.innerHTML = `
${this.icons.error} Error: Failed to execute query
`; } }, renderSqlResult(data, container) { if (data.success) { let tableHtml = ''; if (data.result && data.result.length > 0) { const columns = Object.keys(data.result[0]); tableHtml = `
${columns.map(col => ``).join('')} ${data.result.map(row => ` ${columns.map(col => ` `).join('')} `).join('')}
${this.escapeHtml(col)}
${row[col] === null ? 'NULL' : this.escapeHtml(String(row[col]).substring(0, 100))}
`; } container.innerHTML = `
${this.icons.success} ${this.escapeHtml(data.message)} ${data.execution_time}ms
${tableHtml ? `
${tableHtml}
` : ''}
`; } else { container.innerHTML = `
${this.icons.error} ${this.escapeHtml(data.message)}
${data.error ? `
${this.escapeHtml(data.error)}
` : ''}
`; } this.refreshIcons(); }, initSqlHighlighting() { const textarea = document.getElementById('sql_query'); if (!textarea) return; textarea.addEventListener('input', () => this.highlightSql(textarea)); textarea.addEventListener('scroll', () => this.syncScroll(textarea)); this.highlightSql(textarea); }, highlightSql(textarea) { const highlight = document.getElementById('sql-highlight'); if (!highlight) return; let code = textarea.value; code = this.escapeHtml(code); code = this.applySqlSyntax(code); highlight.innerHTML = code + '\n'; }, applySqlSyntax(code) { const keywords = [ 'SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'NOT', 'IN', 'LIKE', 'BETWEEN', 'ORDER BY', 'GROUP BY', 'HAVING', 'LIMIT', 'OFFSET', 'JOIN', 'INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'OUTER JOIN', 'ON', 'AS', 'DISTINCT', 'ALL', 'INSERT', 'INTO', 'VALUES', 'UPDATE', 'SET', 'DELETE', 'CREATE', 'TABLE', 'DATABASE', 'INDEX', 'VIEW', 'DROP', 'ALTER', 'ADD', 'COLUMN', 'PRIMARY KEY', 'FOREIGN KEY', 'REFERENCES', 'CONSTRAINT', 'DEFAULT', 'NULL', 'NOT NULL', 'AUTO_INCREMENT', 'UNIQUE', 'ENGINE', 'CHARSET', 'COLLATE', 'IF', 'EXISTS', 'SHOW', 'DESCRIBE', 'EXPLAIN', 'USE', 'GRANT', 'REVOKE', 'UNION', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'IS', 'TRUE', 'FALSE', 'ASC', 'DESC' ]; const functions = [ 'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'CONCAT', 'SUBSTRING', 'LENGTH', 'UPPER', 'LOWER', 'TRIM', 'REPLACE', 'NOW', 'CURDATE', 'DATE', 'YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE', 'COALESCE', 'IFNULL', 'NULLIF', 'CAST', 'CONVERT', 'FORMAT', 'ROUND', 'FLOOR', 'CEIL', 'ABS', 'MOD', 'RAND' ]; code = code.replace(/'([^'\\]|\\.)*'/g, '$&'); code = code.replace(/"([^"\\]|\\.)*"/g, '$&'); code = code.replace(/\b(\d+\.?\d*)\b/g, '$1'); functions.forEach(func => { const regex = new RegExp(`\\b(${func})\\s*\\(`, 'gi'); code = code.replace(regex, '$1('); }); keywords.forEach(keyword => { const regex = new RegExp(`\\b(${keyword.replace(' ', '\\s+')})\\b`, 'gi'); code = code.replace(regex, '$1'); }); code = code.replace(/(--[^\n]*)/g, '$1'); code = code.replace(/(\/\*[\s\S]*?\*\/)/g, '$1'); return code; }, syncScroll(textarea) { const highlight = document.getElementById('sql-highlight'); if (highlight) { highlight.scrollTop = textarea.scrollTop; highlight.scrollLeft = textarea.scrollLeft; } }, initAjaxForms() { document.querySelectorAll('[data-ajax-form]').forEach(form => { form.addEventListener('submit', async (e) => { e.preventDefault(); await this.submitAjaxForm(form); }); }); document.querySelectorAll('[data-confirm]').forEach(el => { el.addEventListener('click', (e) => { if (!confirm(el.dataset.confirm)) { e.preventDefault(); } }); }); }, async submitAjaxForm(form) { const formData = new FormData(form); const submitBtn = form.querySelector('[type="submit"]'); const originalText = submitBtn?.innerHTML; if (submitBtn) { submitBtn.disabled = true; submitBtn.innerHTML = ''; } try { const response = await fetch(form.action, { method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' } }); const data = await response.json(); if (data.success) { if (data.redirect) { window.location.href = data.redirect; } else if (data.reload) { window.location.reload(); } else { this.showNotification(data.message, 'success'); } } else { this.showNotification(data.message || 'An error occurred', 'error'); } } catch (error) { this.showNotification('Request failed', 'error'); } finally { if (submitBtn) { submitBtn.disabled = false; submitBtn.innerHTML = originalText; } } }, showNotification(message, type) { const notification = document.createElement('div'); notification.className = `dbm-notification ${type}`; notification.innerHTML = ` ${type === 'success' ? this.icons.success : this.icons.error} ${this.escapeHtml(message)} `; notification.style.cssText = ` position: fixed; bottom: 20px; right: 20px; padding: 12px 20px; background: ${type === 'success' ? 'var(--accent-green)' : 'var(--accent-red)'}; color: #fff; border-radius: var(--radius); display: flex; align-items: center; gap: 10px; font-size: 14px; box-shadow: var(--shadow); z-index: 1000; animation: slideIn 0.3s ease; `; document.body.appendChild(notification); this.refreshIcons(); setTimeout(() => { notification.style.animation = 'slideOut 0.3s ease'; setTimeout(() => notification.remove(), 300); }, 3000); }, escapeHtml(str) { if (!str) return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; }, icons: { chevron: '', database: '', table: '', column: '', key: '', success: '', error: '', terminal: '' }, refreshIcons() { if (typeof lucide !== 'undefined') { lucide.createIcons(); } } }; const style = document.createElement('style'); style.textContent = ` @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } `; document.head.appendChild(style);