Files
ITL-Huge/public/js/dbmanager.js
2026-01-14 23:04:53 +01:00

390 lines
14 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 => {
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);