434 lines
17 KiB
JavaScript
434 lines
17 KiB
JavaScript
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 = '<div class="tree-loading">Loading...</div>';
|
|
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 || '<div class="tree-empty">No tables</div>';
|
|
this.initTree();
|
|
this.refreshIcons();
|
|
}
|
|
} catch (error) {
|
|
container.innerHTML = '<div class="tree-error">Failed to load</div>';
|
|
}
|
|
},
|
|
|
|
async loadColumns(dbName, tableName, container) {
|
|
container.innerHTML = '<div class="tree-loading">Loading...</div>';
|
|
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 += `
|
|
<div class="tree-item">
|
|
<div class="tree-header">
|
|
<span class="tree-icon ${col.Key === 'PRI' ? 'key' : 'column'}">
|
|
${col.Key === 'PRI' ? this.icons.key : this.icons.column}
|
|
</span>
|
|
<span class="tree-label">${this.escapeHtml(col.Field)}</span>
|
|
<span class="tree-badge">${this.escapeHtml(col.Type)}</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
container.innerHTML = html || '<div class="tree-empty">No columns</div>';
|
|
this.refreshIcons();
|
|
}
|
|
} catch (error) {
|
|
container.innerHTML = '<div class="tree-error">Failed to load</div>';
|
|
}
|
|
},
|
|
|
|
renderTableTreeItem(dbName, tableName, columns) {
|
|
const columnsHtml = columns.map(col => `
|
|
<div class="tree-item">
|
|
<div class="tree-header">
|
|
<span class="tree-icon column">
|
|
${col.Key === 'PRI' ? this.icons.key : this.icons.column}
|
|
</span>
|
|
<span class="tree-label">${this.escapeHtml(col.Field)}</span>
|
|
<span class="tree-badge">${this.escapeHtml(col.Type)}</span>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
return `
|
|
<div class="tree-item" data-table="${this.escapeHtml(tableName)}">
|
|
<div class="tree-header" data-href="${this.baseUrl}table/show/${encodeURIComponent(dbName)}/${encodeURIComponent(tableName)}">
|
|
<span class="tree-toggle">${this.icons.chevron}</span>
|
|
<span class="tree-icon table">${this.icons.table}</span>
|
|
<span class="tree-label">${this.escapeHtml(tableName)}</span>
|
|
</div>
|
|
<div class="tree-children">
|
|
${columnsHtml}
|
|
</div>
|
|
</div>
|
|
`;
|
|
},
|
|
|
|
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 = '<div class="dbm-loading"><div class="dbm-spinner"></div></div>';
|
|
|
|
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 = `
|
|
<div class="dbm-sql-result error">
|
|
<div class="dbm-sql-result-header">
|
|
${this.icons.error} Error: Failed to execute query
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
},
|
|
|
|
renderSqlResult(data, container) {
|
|
if (data.success) {
|
|
let tableHtml = '';
|
|
|
|
if (data.result && data.result.length > 0) {
|
|
const columns = Object.keys(data.result[0]);
|
|
tableHtml = `
|
|
<div class="dbm-table-wrapper">
|
|
<table class="dbm-table">
|
|
<thead>
|
|
<tr>${columns.map(col => `<th>${this.escapeHtml(col)}</th>`).join('')}</tr>
|
|
</thead>
|
|
<tbody>
|
|
${data.result.map(row => `
|
|
<tr>${columns.map(col => `
|
|
<td>${row[col] === null ? '<span class="null-value">NULL</span>' : this.escapeHtml(String(row[col]).substring(0, 100))}</td>
|
|
`).join('')}</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
container.innerHTML = `
|
|
<div class="dbm-sql-result success">
|
|
<div class="dbm-sql-result-header">
|
|
${this.icons.success} ${this.escapeHtml(data.message)}
|
|
<span style="margin-left: auto; color: var(--text-muted); font-size: 12px;">
|
|
${data.execution_time}ms
|
|
</span>
|
|
</div>
|
|
${tableHtml ? `<div class="dbm-sql-result-body">${tableHtml}</div>` : ''}
|
|
</div>
|
|
`;
|
|
} else {
|
|
container.innerHTML = `
|
|
<div class="dbm-sql-result error">
|
|
<div class="dbm-sql-result-header">
|
|
${this.icons.error} ${this.escapeHtml(data.message)}
|
|
</div>
|
|
${data.error ? `<div class="dbm-sql-result-body" style="padding: 16px; font-family: monospace; font-size: 13px; color: var(--accent-red);">${this.escapeHtml(data.error)}</div>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
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, '<span class="sql-string">$&</span>');
|
|
code = code.replace(/"([^"\\]|\\.)*"/g, '<span class="sql-string">$&</span>');
|
|
code = code.replace(/\b(\d+\.?\d*)\b/g, '<span class="sql-number">$1</span>');
|
|
|
|
functions.forEach(func => {
|
|
const regex = new RegExp(`\\b(${func})\\s*\\(`, 'gi');
|
|
code = code.replace(regex, '<span class="sql-function">$1</span>(');
|
|
});
|
|
|
|
keywords.forEach(keyword => {
|
|
const regex = new RegExp(`\\b(${keyword.replace(' ', '\\s+')})\\b`, 'gi');
|
|
code = code.replace(regex, '<span class="sql-keyword">$1</span>');
|
|
});
|
|
|
|
code = code.replace(/(--[^\n]*)/g, '<span class="sql-comment">$1</span>');
|
|
code = code.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="sql-comment">$1</span>');
|
|
|
|
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 = '<span class="dbm-spinner" style="width:16px;height:16px;border-width:2px;"></span>';
|
|
}
|
|
|
|
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}
|
|
<span>${this.escapeHtml(message)}</span>
|
|
`;
|
|
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: '<i data-lucide="chevron-right"></i>',
|
|
database: '<i data-lucide="database"></i>',
|
|
table: '<i data-lucide="table"></i>',
|
|
column: '<i data-lucide="columns-2"></i>',
|
|
key: '<i data-lucide="key-round"></i>',
|
|
success: '<i data-lucide="check-circle"></i>',
|
|
error: '<i data-lucide="x-circle"></i>',
|
|
terminal: '<i data-lucide="terminal"></i>'
|
|
},
|
|
|
|
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);
|