Files
ITL-Huge/public/js/dbmanager.js
2026-01-26 10:37:06 +01:00

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);