const DBManager = { baseUrl: '', init(baseUrl) { this.baseUrl = baseUrl; this.initTree(); this.initConsole(); this.initSqlHighlighting(); this.initAjaxForms(); }, initTree() { document.querySelectorAll('.tree-header').forEach(header => { 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 if (toggle) { e.preventDefault(); e.stopPropagation(); if (item.dataset.db && children && !children.dataset.loaded) { this.loadTables(item.dataset.db, children); } this.toggleTreeItem(item); return; } // If it's a table or database item with href, navigate directly if (href) { // For databases, also load tables if not loaded 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
'; } }, 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);