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 += `
`;
});
container.innerHTML = html || 'No columns
';
this.refreshIcons();
}
} catch (error) {
container.innerHTML = 'Failed to load
';
}
},
renderTableTreeItem(dbName, tableName, columns) {
const columnsHtml = columns.map(col => `
`).join('');
return `
`;
},
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 = `
`;
}
},
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 => `| ${this.escapeHtml(col)} | `).join('')}
${data.result.map(row => `
${columns.map(col => `
| ${row[col] === null ? 'NULL' : this.escapeHtml(String(row[col]).substring(0, 100))} |
`).join('')}
`).join('')}
`;
}
container.innerHTML = `
${tableHtml ? `
${tableHtml}
` : ''}
`;
} else {
container.innerHTML = `
${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, '');
code = code.replace(/(\/\*[\s\S]*?\*\/)/g, '');
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);