Initial commit
This commit is contained in:
389
public/js/dbmanager.js
Normal file
389
public/js/dbmanager.js
Normal file
@@ -0,0 +1,389 @@
|
||||
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 = '<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>';
|
||||
}
|
||||
},
|
||||
|
||||
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);
|
||||
Reference in New Issue
Block a user