1628 lines
59 KiB
JavaScript
1628 lines
59 KiB
JavaScript
// JavaScript für Query Builder Funktionalität
|
|
|
|
// Globale Variablen
|
|
let currentTables = [];
|
|
let savedQueries = [];
|
|
let currentConnection = 'oracle';
|
|
|
|
// Lade verfügbare Datenbankverbindungen
|
|
function loadConnections() {
|
|
fetch('/get_connections')
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
const selector = document.getElementById('database-selector');
|
|
if (!selector) {
|
|
console.warn('Database selector nicht gefunden');
|
|
return;
|
|
}
|
|
|
|
selector.innerHTML = '';
|
|
|
|
if (data.success && data.connections && data.connections.length > 0) {
|
|
data.connections.forEach(conn => {
|
|
const option = document.createElement('option');
|
|
option.value = conn.key;
|
|
option.textContent = `${conn.name} (${conn.type})`;
|
|
option.title = conn.description;
|
|
if (conn.key === 'oracle') {
|
|
option.selected = true;
|
|
currentConnection = 'oracle';
|
|
}
|
|
selector.appendChild(option);
|
|
});
|
|
|
|
// Lade Tabellen für die ausgewählte Verbindung
|
|
loadTables();
|
|
} else {
|
|
const option = document.createElement('option');
|
|
option.textContent = data.error || 'Keine Verbindungen verfügbar';
|
|
selector.appendChild(option);
|
|
console.error('API-Fehler:', data.error);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Netzwerk-Fehler beim Laden der Verbindungen:', error);
|
|
const selector = document.getElementById('database-selector');
|
|
if (selector) {
|
|
selector.innerHTML = `<option>Fehler: ${error.message}</option>`;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Datenbankauswahl geändert
|
|
function onDatabaseChange() {
|
|
const selector = document.getElementById('database-selector');
|
|
if (selector) {
|
|
currentConnection = selector.value;
|
|
loadTables();
|
|
}
|
|
}
|
|
|
|
// Utility Functions
|
|
function showLoading(elementId) {
|
|
const element = document.getElementById(elementId);
|
|
if (element) {
|
|
element.innerHTML = '<div class="text-center"><div class="loading"></div> Lade...</div>';
|
|
}
|
|
}
|
|
|
|
function showAlert(message, type = 'info') {
|
|
const alertClass = type === 'error' ? 'alert-error-custom' : 'alert-success-custom';
|
|
const alertHtml = `
|
|
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
|
|
${message}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
`;
|
|
|
|
// Füge Alert am Anfang des Main-Containers hinzu
|
|
const mainContent = document.querySelector('main .container-fluid');
|
|
if (mainContent) {
|
|
const alertDiv = document.createElement('div');
|
|
alertDiv.innerHTML = alertHtml;
|
|
mainContent.insertBefore(alertDiv.firstElementChild, mainContent.firstElementChild);
|
|
}
|
|
}
|
|
|
|
// Tabellen laden
|
|
async function loadTables() {
|
|
showLoading('tables-list');
|
|
|
|
// Aktuelle Datenbankverbindung bestimmen
|
|
const connectionSelector = document.getElementById('database-selector');
|
|
const connection = connectionSelector ? connectionSelector.value : 'oracle';
|
|
|
|
try {
|
|
const response = await fetch(`/get_tables?connection=${connection}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
currentTables = data.tables;
|
|
displayTables(data.tables);
|
|
} else {
|
|
document.getElementById('tables-list').innerHTML =
|
|
`<div class="text-danger">Fehler: ${data.error}</div>`;
|
|
}
|
|
} catch (error) {
|
|
document.getElementById('tables-list').innerHTML =
|
|
`<div class="text-danger">Fehler beim Laden der Tabellen: ${error.message}</div>`;
|
|
}
|
|
}
|
|
|
|
function displayTables(tables) {
|
|
const tablesList = document.getElementById('tables-list');
|
|
|
|
if (tables.length === 0) {
|
|
tablesList.innerHTML = '<div class="text-muted">Keine Tabellen gefunden</div>';
|
|
return;
|
|
}
|
|
|
|
const tablesHtml = tables.map(table => `
|
|
<div class="table-container">
|
|
<div class="table-item"
|
|
onclick="selectTable('${table}')"
|
|
data-table="${table}"
|
|
draggable="true"
|
|
ondragstart="handleTableDrag(event, '${table}')">
|
|
<i class="fas fa-table"></i> ${table}
|
|
<i class="fas fa-arrows-alt drag-icon"></i>
|
|
</div>
|
|
<div class="table-columns" id="columns-${table}" style="display: none;"></div>
|
|
</div>
|
|
`).join('');
|
|
|
|
tablesList.innerHTML = tablesHtml;
|
|
|
|
// Event-Listener für Hover-Effekte hinzufügen (nur wenn Spalten-Anzeige deaktiviert ist)
|
|
setTimeout(() => attachTableHoverEvents(), 100);
|
|
}
|
|
|
|
// Tabelle auswählen und SELECT Query generieren
|
|
async function selectTable(tableName) {
|
|
const queryInput = document.getElementById('query-input');
|
|
const currentQuery = queryInput.value.trim();
|
|
|
|
// Wenn bereits eine Query vorhanden ist, frage nach
|
|
if (currentQuery) {
|
|
if (!confirm('Dies wird die aktuelle Query ersetzen. Fortfahren?')) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
queryInput.value = `SELECT * FROM ${tableName} WHERE ROWNUM <= 100`;
|
|
|
|
// Alle anderen Tabellen-Spalten verstecken
|
|
document.querySelectorAll('.table-columns').forEach(col => {
|
|
col.style.display = 'none';
|
|
});
|
|
|
|
// Aktuelle Tabelle als ausgewählt markieren
|
|
document.querySelectorAll('.table-item').forEach(item => {
|
|
item.classList.remove('selected');
|
|
});
|
|
|
|
// Die geklickte Tabelle markieren
|
|
const tableItems = document.querySelectorAll('.table-item');
|
|
tableItems.forEach(item => {
|
|
if (item.textContent.includes(tableName)) {
|
|
item.classList.add('selected');
|
|
}
|
|
});
|
|
|
|
// Spalten für die ausgewählte Tabelle laden und anzeigen
|
|
await showTableColumns(tableName);
|
|
}
|
|
|
|
// Funktion zum dauerhaften Anzeigen der Tabellenspalten
|
|
async function showTableColumns(tableName) {
|
|
try {
|
|
const connectionSelector = document.getElementById('database-selector');
|
|
const connection = connectionSelector ? connectionSelector.value : 'oracle';
|
|
|
|
const response = await fetch(`/get_table_schema/${tableName}?connection=${connection}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
const columnsContainer = document.getElementById(`columns-${tableName}`);
|
|
if (columnsContainer) {
|
|
const columnsHtml = data.schema.map(col => `
|
|
<div class="column-item"
|
|
onclick="insertColumn('${col.name}')"
|
|
draggable="true"
|
|
ondragstart="handleColumnDrag(event, '${tableName}', '${col.name}')">
|
|
<i class="fas fa-columns text-muted"></i>
|
|
<span class="column-name">${col.name}</span>
|
|
<small class="column-type text-muted">${col.type}</small>
|
|
${col.primary_key ? '<i class="fas fa-key text-warning ms-1" title="Primary Key"></i>' : ''}
|
|
${col.not_null ? '<i class="fas fa-exclamation text-info ms-1" title="Not Null"></i>' : ''}
|
|
<i class="fas fa-arrows-alt drag-icon"></i>
|
|
</div>
|
|
`).join('');
|
|
|
|
columnsContainer.innerHTML = columnsHtml;
|
|
// Nur anzeigen wenn Spalten-View aktiv ist
|
|
columnsContainer.style.display = columnsVisible ? 'block' : 'none';
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Spalten:', error);
|
|
}
|
|
}
|
|
|
|
// Funktion zum Einfügen einer Spalte in die Query
|
|
function insertColumn(columnName) {
|
|
const queryInput = document.getElementById('query-input');
|
|
const cursorPos = queryInput.selectionStart;
|
|
const currentQuery = queryInput.value;
|
|
|
|
// Wenn SELECT * vorhanden ist, ersetze es durch die spezifische Spalte
|
|
if (currentQuery.includes('SELECT *')) {
|
|
queryInput.value = currentQuery.replace('SELECT *', `SELECT ${columnName}`);
|
|
} else {
|
|
// Andernfalls füge die Spalte an der Cursor-Position ein
|
|
const beforeCursor = currentQuery.substring(0, cursorPos);
|
|
const afterCursor = currentQuery.substring(cursorPos);
|
|
queryInput.value = beforeCursor + columnName + afterCursor;
|
|
|
|
// Cursor-Position nach dem eingefügten Spaltennamen setzen
|
|
queryInput.selectionStart = queryInput.selectionEnd = cursorPos + columnName.length;
|
|
}
|
|
|
|
queryInput.focus();
|
|
}
|
|
|
|
// Drag & Drop Funktionen
|
|
function handleTableDrag(event, tableName) {
|
|
event.dataTransfer.setData('text/plain', `SELECT * FROM ${tableName}`);
|
|
event.dataTransfer.setData('application/x-table-name', tableName);
|
|
event.dataTransfer.effectAllowed = 'copy';
|
|
|
|
// Custom Drag Image
|
|
const dragImage = document.createElement('div');
|
|
dragImage.innerHTML = `<i class="fas fa-table"></i> ${tableName}`;
|
|
dragImage.style.cssText = 'padding: 8px 12px; background: #007bff; color: white; border-radius: 4px; position: absolute; top: -1000px;';
|
|
document.body.appendChild(dragImage);
|
|
event.dataTransfer.setDragImage(dragImage, 0, 0);
|
|
|
|
setTimeout(() => document.body.removeChild(dragImage), 0);
|
|
}
|
|
|
|
function handleColumnDrag(event, tableName, columnName) {
|
|
event.dataTransfer.setData('text/plain', `${tableName}.${columnName}`);
|
|
event.dataTransfer.setData('application/x-column-name', columnName);
|
|
event.dataTransfer.setData('application/x-table-name', tableName);
|
|
event.dataTransfer.effectAllowed = 'copy';
|
|
|
|
// Custom Drag Image
|
|
const dragImage = document.createElement('div');
|
|
dragImage.innerHTML = `<i class="fas fa-columns"></i> ${columnName}`;
|
|
dragImage.style.cssText = 'padding: 6px 10px; background: #28a745; color: white; border-radius: 4px; position: absolute; top: -1000px; font-size: 12px;';
|
|
document.body.appendChild(dragImage);
|
|
event.dataTransfer.setDragImage(dragImage, 0, 0);
|
|
|
|
setTimeout(() => document.body.removeChild(dragImage), 0);
|
|
}
|
|
|
|
// Setup Drag & Drop für Query Input Feld
|
|
function setupQueryInputDropZone(queryInput) {
|
|
queryInput.addEventListener('dragover', function(event) {
|
|
event.preventDefault();
|
|
event.dataTransfer.dropEffect = 'copy';
|
|
this.classList.add('drag-over');
|
|
});
|
|
|
|
queryInput.addEventListener('dragleave', function(event) {
|
|
event.preventDefault();
|
|
this.classList.remove('drag-over');
|
|
});
|
|
|
|
queryInput.addEventListener('drop', function(event) {
|
|
event.preventDefault();
|
|
this.classList.remove('drag-over');
|
|
|
|
const droppedText = event.dataTransfer.getData('text/plain');
|
|
const tableName = event.dataTransfer.getData('application/x-table-name');
|
|
const columnName = event.dataTransfer.getData('application/x-column-name');
|
|
|
|
if (droppedText) {
|
|
// Cursor-Position ermitteln
|
|
const cursorPos = this.selectionStart;
|
|
const currentQuery = this.value;
|
|
|
|
// Text an Cursor-Position einfügen
|
|
const beforeCursor = currentQuery.substring(0, cursorPos);
|
|
const afterCursor = currentQuery.substring(cursorPos);
|
|
|
|
let insertText = droppedText;
|
|
|
|
// Intelligentes Einfügen basierend auf Kontext
|
|
if (columnName && !tableName.includes('.')) {
|
|
// Wenn es eine Spalte ist, prüfe ob wir eine qualifizierte Referenz brauchen
|
|
const needsTablePrefix = shouldUseTablePrefix(currentQuery, cursorPos);
|
|
insertText = needsTablePrefix ? `${tableName}.${columnName}` : columnName;
|
|
}
|
|
|
|
// Füge Leerzeichen hinzu wenn nötig
|
|
if (beforeCursor && !beforeCursor.endsWith(' ') && !beforeCursor.endsWith('\n')) {
|
|
insertText = ' ' + insertText;
|
|
}
|
|
if (afterCursor && !afterCursor.startsWith(' ') && !afterCursor.startsWith('\n')) {
|
|
insertText = insertText + ' ';
|
|
}
|
|
|
|
this.value = beforeCursor + insertText + afterCursor;
|
|
|
|
// Cursor nach dem eingefügten Text positionieren
|
|
const newCursorPos = cursorPos + insertText.length;
|
|
this.selectionStart = this.selectionEnd = newCursorPos;
|
|
|
|
// Focus setzen
|
|
this.focus();
|
|
|
|
// Auto-resize triggern
|
|
this.style.height = 'auto';
|
|
this.style.height = Math.max(150, this.scrollHeight) + 'px';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Hilfsfunktion um zu bestimmen ob ein Tabellenpräfix nötig ist
|
|
function shouldUseTablePrefix(query, cursorPos) {
|
|
const beforeCursor = query.substring(0, cursorPos).toUpperCase();
|
|
|
|
// Wenn mehrere FROM-Klauseln vorhanden sind, verwende Tabellenpräfix
|
|
const fromCount = (beforeCursor.match(/\bFROM\b/g) || []).length;
|
|
const joinCount = (beforeCursor.match(/\bJOIN\b/g) || []).length;
|
|
|
|
return fromCount > 1 || joinCount > 0;
|
|
}
|
|
|
|
// Event-Listener für Hover-Effekte an Tabellen-Items anhängen
|
|
function attachTableHoverEvents() {
|
|
const tableItems = document.querySelectorAll('.table-item');
|
|
|
|
tableItems.forEach(item => {
|
|
const tableName = item.getAttribute('data-table');
|
|
|
|
// Entferne vorherige Event-Listener
|
|
item.removeEventListener('mouseenter', item._hoverHandler);
|
|
item.removeEventListener('mouseleave', item._leaveHandler);
|
|
|
|
// Neue Event-Listener nur hinzufügen, wenn Spalten-Anzeige deaktiviert ist
|
|
if (!columnsVisible) {
|
|
item._hoverHandler = () => showTableSchema(tableName, item);
|
|
item._leaveHandler = () => hideTableSchema();
|
|
|
|
item.addEventListener('mouseenter', item._hoverHandler);
|
|
item.addEventListener('mouseleave', item._leaveHandler);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Tabellenschema anzeigen
|
|
async function showTableSchema(tableName, element) {
|
|
try {
|
|
// Aktuelle Datenbankverbindung bestimmen
|
|
const connectionSelector = document.getElementById('database-selector');
|
|
const connection = connectionSelector ? connectionSelector.value : 'demo';
|
|
|
|
const response = await fetch(`/get_table_schema/${tableName}?connection=${connection}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
const rect = element.getBoundingClientRect();
|
|
const schemaHtml = data.schema.map(col => `
|
|
<div class="schema-column">
|
|
<strong>${col.name}</strong> <span class="text-muted">${col.type}</span>
|
|
${col.primary_key ? '<i class="fas fa-key text-warning" title="Primary Key"></i>' : ''}
|
|
${col.not_null ? '<i class="fas fa-exclamation text-info" title="Not Null"></i>' : ''}
|
|
</div>
|
|
`).join('');
|
|
|
|
const popup = document.createElement('div');
|
|
popup.className = 'schema-popup';
|
|
popup.id = 'schema-popup';
|
|
popup.innerHTML = `<h6>${tableName}</h6>${schemaHtml}`;
|
|
popup.style.left = (rect.right + 10) + 'px';
|
|
popup.style.top = rect.top + 'px';
|
|
|
|
document.body.appendChild(popup);
|
|
}
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden des Schemas:', error);
|
|
}
|
|
}
|
|
|
|
function hideTableSchema() {
|
|
const popup = document.getElementById('schema-popup');
|
|
if (popup) {
|
|
popup.remove();
|
|
}
|
|
}
|
|
|
|
// Query ausführen
|
|
async function executeQuery() {
|
|
const queryInput = document.getElementById('query-input');
|
|
const query = queryInput.value.trim();
|
|
|
|
if (!query) {
|
|
showAlert('Bitte geben Sie eine Query ein', 'error');
|
|
return;
|
|
}
|
|
|
|
const resultsContainer = document.getElementById('results-container');
|
|
resultsContainer.innerHTML = `
|
|
<div class="card-body text-center">
|
|
<div class="loading"></div>
|
|
<p class="mt-2">Query wird ausgeführt...</p>
|
|
</div>
|
|
`;
|
|
|
|
try {
|
|
// Aktuelle Datenbankverbindung bestimmen
|
|
const connectionSelector = document.getElementById('database-selector');
|
|
const connection = connectionSelector ? connectionSelector.value : 'oracle';
|
|
|
|
const response = await fetch('/execute_query', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
query: query,
|
|
connection: connection
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
displayQueryResults(data.results);
|
|
showAlert('Query erfolgreich ausgeführt');
|
|
} else {
|
|
resultsContainer.innerHTML = `
|
|
<div class="card-body">
|
|
<div class="alert alert-danger">
|
|
<h6>Query-Fehler:</h6>
|
|
<pre class="mb-0">${data.error}</pre>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
} catch (error) {
|
|
resultsContainer.innerHTML = `
|
|
<div class="card-body">
|
|
<div class="alert alert-danger">
|
|
<h6>Netzwerk-Fehler:</h6>
|
|
<p class="mb-0">${error.message}</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function displayQueryResults(results) {
|
|
const resultsContainer = document.getElementById('results-container');
|
|
|
|
if (results.message) {
|
|
// Für INSERT, UPDATE, DELETE etc.
|
|
resultsContainer.innerHTML = `
|
|
<div class="card-body">
|
|
<div class="alert alert-success">
|
|
<i class="fas fa-check-circle"></i> ${results.message}
|
|
</div>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
if (!results.data || results.data.length === 0) {
|
|
resultsContainer.innerHTML = `
|
|
<div class="card-body">
|
|
<div class="alert alert-info">
|
|
<i class="fas fa-info-circle"></i> Query erfolgreich, aber keine Ergebnisse gefunden.
|
|
</div>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
// Erstelle Tabelle mit Ergebnissen
|
|
const tableHtml = `
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6 class="mb-0">
|
|
<i class="fas fa-table"></i>
|
|
${results.row_count} Zeile(n) gefunden
|
|
</h6>
|
|
<button class="btn btn-sm btn-outline-success" onclick="exportResults('csv')">
|
|
<i class="fas fa-download"></i> CSV Export
|
|
</button>
|
|
</div>
|
|
<div class="results-table">
|
|
<table class="table table-striped table-hover">
|
|
<thead>
|
|
<tr>
|
|
${results.columns.map(col => `<th>${col}</th>`).join('')}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${results.data.map(row => `
|
|
<tr>
|
|
${row.map(cell => `<td>${cell !== null ? cell : '<em class="text-muted">NULL</em>'}</td>`).join('')}
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
resultsContainer.innerHTML = tableHtml;
|
|
}
|
|
|
|
// Query speichern
|
|
function showSaveQueryModal(event) {
|
|
if (event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
|
|
const queryInput = document.getElementById('query-input');
|
|
const query = queryInput.value.trim();
|
|
|
|
if (!query) {
|
|
showAlert('Bitte geben Sie eine Query ein', 'error');
|
|
return false;
|
|
}
|
|
|
|
// Reset des Modal-Formulars
|
|
const queryNameInput = document.getElementById('query-name');
|
|
if (queryNameInput) {
|
|
queryNameInput.value = '';
|
|
}
|
|
|
|
// Modal anzeigen - alternative Methode
|
|
const modalElement = document.getElementById('saveQueryModal');
|
|
if (modalElement) {
|
|
// Prüfe ob Bootstrap verfügbar ist
|
|
if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
|
|
const modal = new bootstrap.Modal(modalElement, {
|
|
backdrop: true,
|
|
keyboard: true,
|
|
focus: true
|
|
});
|
|
modal.show();
|
|
|
|
// Fokus auf Name-Input setzen nach Modal-Öffnung
|
|
modalElement.addEventListener('shown.bs.modal', function() {
|
|
const nameInput = document.getElementById('query-name');
|
|
if (nameInput) {
|
|
nameInput.focus();
|
|
}
|
|
}, { once: true });
|
|
|
|
} else {
|
|
// Fallback: Zeige Modal manuell
|
|
modalElement.style.display = 'block';
|
|
modalElement.classList.add('show');
|
|
modalElement.setAttribute('aria-modal', 'true');
|
|
modalElement.setAttribute('role', 'dialog');
|
|
document.body.classList.add('modal-open');
|
|
|
|
// Backdrop hinzufügen
|
|
const backdrop = document.createElement('div');
|
|
backdrop.className = 'modal-backdrop fade show';
|
|
backdrop.id = 'manual-modal-backdrop';
|
|
document.body.appendChild(backdrop);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Funktion zum manuellen Schließen des Modals (Fallback)
|
|
function closeModalManually() {
|
|
const modalElement = document.getElementById('saveQueryModal');
|
|
const backdrop = document.getElementById('manual-modal-backdrop');
|
|
|
|
if (modalElement) {
|
|
modalElement.style.display = 'none';
|
|
modalElement.classList.remove('show');
|
|
modalElement.removeAttribute('aria-modal');
|
|
modalElement.removeAttribute('role');
|
|
document.body.classList.remove('modal-open');
|
|
}
|
|
|
|
if (backdrop) {
|
|
backdrop.remove();
|
|
}
|
|
}
|
|
|
|
async function saveQuery() {
|
|
const name = document.getElementById('query-name').value.trim();
|
|
const query = document.getElementById('query-input').value.trim();
|
|
|
|
if (!name || !query) {
|
|
showAlert('Name und Query sind erforderlich', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/save_query', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
name: name,
|
|
description: '', // Keine Beschreibung mehr
|
|
query: query
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showAlert('Query erfolgreich gespeichert');
|
|
|
|
// Modal schließen
|
|
if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('saveQueryModal'));
|
|
if (modal) {
|
|
modal.hide();
|
|
} else {
|
|
closeModalManually();
|
|
}
|
|
} else {
|
|
closeModalManually();
|
|
}
|
|
|
|
// Input-Feld zurücksetzen
|
|
document.getElementById('query-name').value = '';
|
|
|
|
// Seite neu laden um gespeicherte Queries zu aktualisieren
|
|
setTimeout(() => window.location.reload(), 1000);
|
|
} else {
|
|
showAlert(data.error, 'error');
|
|
}
|
|
} catch (error) {
|
|
showAlert(`Fehler beim Speichern: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// Gespeicherte Query laden
|
|
function loadSavedQuery(queryId, queryText) {
|
|
const queryInput = document.getElementById('query-input');
|
|
|
|
if (queryInput.value.trim() && !confirm('Dies wird die aktuelle Query ersetzen. Fortfahren?')) {
|
|
return;
|
|
}
|
|
|
|
queryInput.value = queryText;
|
|
}
|
|
|
|
// Gespeicherte Query löschen
|
|
async function deleteSavedQuery(queryId) {
|
|
if (!confirm('Möchten Sie diese gespeicherte Query wirklich löschen?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/delete_query/${queryId}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showAlert('Query erfolgreich gelöscht');
|
|
|
|
// Query-Element entfernen
|
|
const queryCard = document.querySelector(`[data-query-id="${queryId}"]`);
|
|
if (queryCard) {
|
|
queryCard.remove();
|
|
}
|
|
} else {
|
|
showAlert(data.error, 'error');
|
|
}
|
|
} catch (error) {
|
|
showAlert(`Fehler beim Löschen: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// Neue Funktionen für die verbesserte Query-Verwaltung
|
|
|
|
// Gespeicherte Queries filtern
|
|
function filterSavedQueries() {
|
|
const searchTerm = document.getElementById('search-queries').value.toLowerCase();
|
|
const queryCards = document.querySelectorAll('.saved-query-card');
|
|
|
|
queryCards.forEach(card => {
|
|
const queryName = card.querySelector('h6').textContent.toLowerCase();
|
|
const queryText = card.dataset.queryText ? card.dataset.queryText.toLowerCase() : '';
|
|
|
|
if (queryName.includes(searchTerm) || queryText.includes(searchTerm)) {
|
|
card.style.display = 'block';
|
|
} else {
|
|
card.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Query-Name bearbeiten
|
|
let currentEditQueryId = null;
|
|
|
|
function editQueryName(queryId, currentName) {
|
|
currentEditQueryId = queryId;
|
|
document.getElementById('edit-query-name').value = currentName;
|
|
|
|
// Modal öffnen
|
|
if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
|
|
const modal = new bootstrap.Modal(document.getElementById('editQueryModal'));
|
|
modal.show();
|
|
} else {
|
|
document.getElementById('editQueryModal').style.display = 'block';
|
|
}
|
|
}
|
|
|
|
async function updateQueryName() {
|
|
const newName = document.getElementById('edit-query-name').value.trim();
|
|
|
|
if (!newName) {
|
|
showAlert('Name ist erforderlich', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/update_query_name/${currentEditQueryId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ name: newName })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showAlert('Query-Name erfolgreich aktualisiert');
|
|
|
|
// Modal schließen
|
|
if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('editQueryModal'));
|
|
if (modal) {
|
|
modal.hide();
|
|
}
|
|
} else {
|
|
document.getElementById('editQueryModal').style.display = 'none';
|
|
}
|
|
|
|
// Seite neu laden
|
|
setTimeout(() => window.location.reload(), 1000);
|
|
} else {
|
|
showAlert(data.error, 'error');
|
|
}
|
|
} catch (error) {
|
|
showAlert(`Fehler beim Aktualisieren: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// Query duplizieren
|
|
async function duplicateQuery(queryId, queryName, queryText) {
|
|
const newName = prompt(`Neuer Name für die Kopie von "${queryName}":`, `${queryName} (Kopie)`);
|
|
|
|
if (!newName || newName.trim() === '') {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/save_query', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
name: newName.trim(),
|
|
description: '',
|
|
query: queryText
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showAlert('Query erfolgreich dupliziert');
|
|
setTimeout(() => window.location.reload(), 1000);
|
|
} else {
|
|
showAlert(data.error, 'error');
|
|
}
|
|
} catch (error) {
|
|
showAlert(`Fehler beim Duplizieren: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// Query per Name ausführen (für API)
|
|
async function executeQueryByName(queryName) {
|
|
try {
|
|
// Öffne Ergebnisse in neuem Tab als CSV
|
|
window.open(`/api/queries/${encodeURIComponent(queryName)}/export/csv`, '_blank');
|
|
} catch (error) {
|
|
showAlert(`Fehler beim Export: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
// Query löschen
|
|
function clearQuery() {
|
|
if (confirm('Möchten Sie die aktuelle Query löschen?')) {
|
|
document.getElementById('query-input').value = '';
|
|
}
|
|
}
|
|
|
|
// Ergebnisse exportieren
|
|
function exportResults(format) {
|
|
const query = document.getElementById('query-input').value.trim();
|
|
|
|
if (!query) {
|
|
showAlert('Keine Query zum Exportieren vorhanden', 'error');
|
|
return;
|
|
}
|
|
|
|
// Erstelle temporäre Form für Export
|
|
const form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = '/execute_query';
|
|
form.style.display = 'none';
|
|
|
|
const queryField = document.createElement('input');
|
|
queryField.name = 'query';
|
|
queryField.value = query;
|
|
|
|
const formatField = document.createElement('input');
|
|
formatField.name = 'format';
|
|
formatField.value = format;
|
|
|
|
form.appendChild(queryField);
|
|
form.appendChild(formatField);
|
|
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
document.body.removeChild(form);
|
|
}
|
|
|
|
// Keyboard Shortcuts
|
|
document.addEventListener('keydown', function(e) {
|
|
// Ctrl+Enter oder Cmd+Enter für Query ausführen
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
e.preventDefault();
|
|
executeQuery();
|
|
}
|
|
|
|
// Ctrl+S oder Cmd+S für Query speichern
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
|
e.preventDefault();
|
|
showSaveQueryModal();
|
|
}
|
|
});
|
|
|
|
// Auto-resize für Textarea
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Lade Verbindungen beim Seitenstart
|
|
loadConnections();
|
|
|
|
// Spalten-Anzeige basierend auf gespeicherten Einstellungen initialisieren
|
|
updateColumnsView();
|
|
|
|
const queryInput = document.getElementById('query-input');
|
|
if (queryInput) {
|
|
queryInput.addEventListener('input', function() {
|
|
this.style.height = 'auto';
|
|
this.style.height = Math.max(150, this.scrollHeight) + 'px';
|
|
});
|
|
|
|
// Drag & Drop Event-Listener für Query-Input
|
|
setupQueryInputDropZone(queryInput);
|
|
}
|
|
|
|
// Event-Listener für Save Query Buttons
|
|
const saveQueryBtn = document.getElementById('save-query-btn');
|
|
if (saveQueryBtn) {
|
|
saveQueryBtn.addEventListener('click', function(event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
showSaveQueryModal(event);
|
|
});
|
|
}
|
|
|
|
const saveQueryBtnSidebar = document.getElementById('save-query-btn-sidebar');
|
|
if (saveQueryBtnSidebar) {
|
|
saveQueryBtnSidebar.addEventListener('click', function(event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
showSaveQueryModal(event);
|
|
});
|
|
}
|
|
});
|
|
|
|
// Globale Variable für Spalten-Anzeige Status
|
|
let columnsVisible = localStorage.getItem('columnsVisible') !== 'false'; // Standard: true
|
|
|
|
// Funktion zum Ein-/Ausblenden der Spalten-Anzeige
|
|
function toggleColumnsView() {
|
|
columnsVisible = !columnsVisible;
|
|
localStorage.setItem('columnsVisible', columnsVisible.toString());
|
|
updateColumnsView();
|
|
}
|
|
|
|
// Funktion zum Aktualisieren der Spalten-Anzeige basierend auf dem Status
|
|
function updateColumnsView() {
|
|
const toggleBtn = document.getElementById('toggle-columns-btn');
|
|
const allColumns = document.querySelectorAll('.table-columns');
|
|
|
|
if (columnsVisible) {
|
|
// Spalten anzeigen
|
|
allColumns.forEach(col => {
|
|
if (col.parentElement.querySelector('.table-item.selected')) {
|
|
col.style.display = 'block';
|
|
}
|
|
});
|
|
if (toggleBtn) {
|
|
toggleBtn.innerHTML = '<i class="fas fa-eye"></i>';
|
|
toggleBtn.classList.remove('btn-outline-secondary');
|
|
toggleBtn.classList.add('btn-outline-primary');
|
|
toggleBtn.title = 'Spalten ausblenden';
|
|
}
|
|
} else {
|
|
// Spalten verstecken
|
|
allColumns.forEach(col => {
|
|
col.style.display = 'none';
|
|
});
|
|
if (toggleBtn) {
|
|
toggleBtn.innerHTML = '<i class="fas fa-eye-slash"></i>';
|
|
toggleBtn.classList.remove('btn-outline-primary');
|
|
toggleBtn.classList.add('btn-outline-secondary');
|
|
toggleBtn.title = 'Spalten einblenden';
|
|
}
|
|
}
|
|
|
|
// Hover-Events entsprechend aktualisieren
|
|
attachTableHoverEvents();
|
|
|
|
// Eventuelle Popups schließen
|
|
hideTableSchema();
|
|
}
|
|
|
|
// ============= Autocomplete Funktionalität =============
|
|
|
|
let autocompleteState = {
|
|
isVisible: false,
|
|
selectedIndex: -1,
|
|
items: [],
|
|
triggerPosition: 0,
|
|
type: null // 'table' oder 'column'
|
|
};
|
|
|
|
// Initialisiere Autocomplete Event-Listener
|
|
function initializeAutocomplete() {
|
|
const queryInput = document.getElementById('query-input');
|
|
const autocompleteDropdown = document.getElementById('autocomplete-dropdown');
|
|
|
|
if (!queryInput || !autocompleteDropdown) return;
|
|
|
|
// Event-Listener für Textänderungen
|
|
queryInput.addEventListener('input', handleQueryInputChange);
|
|
queryInput.addEventListener('keydown', handleQueryInputKeydown);
|
|
|
|
// Event-Listener zum Schließen bei Klick außerhalb
|
|
document.addEventListener('click', function(event) {
|
|
if (!queryInput.contains(event.target) && !autocompleteDropdown.contains(event.target)) {
|
|
hideAutocomplete();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Behandle Textänderungen im Query-Input
|
|
function handleQueryInputChange(event) {
|
|
const input = event.target;
|
|
const value = input.value;
|
|
const cursorPosition = input.selectionStart;
|
|
|
|
// Prüfe ob Spalten-Autocomplete nach SELECT angezeigt werden soll
|
|
if (shouldShowColumnAutocomplete(value, cursorPosition)) {
|
|
showColumnAutocomplete(value, cursorPosition);
|
|
}
|
|
// Prüfe ob Tabellen-Autocomplete nach FROM angezeigt werden soll
|
|
else if (shouldShowTableAutocomplete(value, cursorPosition)) {
|
|
showTableAutocomplete(cursorPosition);
|
|
} else {
|
|
hideAutocomplete();
|
|
}
|
|
}
|
|
|
|
// Behandle Tastatureingaben im Query-Input
|
|
function handleQueryInputKeydown(event) {
|
|
if (!autocompleteState.isVisible) return;
|
|
|
|
switch (event.key) {
|
|
case 'ArrowDown':
|
|
event.preventDefault();
|
|
moveAutocompleteSelection(1);
|
|
break;
|
|
case 'ArrowUp':
|
|
event.preventDefault();
|
|
moveAutocompleteSelection(-1);
|
|
break;
|
|
case 'Enter':
|
|
case 'Tab':
|
|
event.preventDefault();
|
|
selectAutocompleteItem();
|
|
break;
|
|
case 'Escape':
|
|
hideAutocomplete();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Prüfe ob Spalten-Autocomplete angezeigt werden soll
|
|
function shouldShowColumnAutocomplete(text, cursorPosition) {
|
|
const textBeforeCursor = text.substring(0, cursorPosition).toUpperCase();
|
|
|
|
console.log('shouldShowColumnAutocomplete checking:', textBeforeCursor); // Debug
|
|
|
|
// Suche nach SELECT gefolgt von optionalem Leerzeichen
|
|
const selectMatch = textBeforeCursor.match(/\bSELECT\s*$/);
|
|
if (selectMatch) {
|
|
console.log('SELECT pattern matched: SELECT at end'); // Debug
|
|
autocompleteState.triggerPosition = cursorPosition;
|
|
return true;
|
|
}
|
|
|
|
// Suche nach SELECT gefolgt von Spalten (mit Kommas) und angefangener neuer Spalte
|
|
const columnMatch = textBeforeCursor.match(/\bSELECT\s+((?:[A-Z_0-9*]+(?:\s*,\s*)?)*)\s*([A-Z_0-9]*)$/);
|
|
if (columnMatch) {
|
|
console.log('SELECT pattern matched: column selection'); // Debug
|
|
autocompleteState.triggerPosition = cursorPosition - (columnMatch[2] ? columnMatch[2].length : 0);
|
|
return true;
|
|
}
|
|
|
|
// Suche nach Komma in der SELECT-Klausel (vor FROM)
|
|
const commaMatch = textBeforeCursor.match(/\bSELECT\s+[^,]+(?:\s*,\s*[^,]*)*\s*,\s*([A-Z_0-9]*)$/);
|
|
if (commaMatch && !textBeforeCursor.includes(' FROM ')) {
|
|
console.log('SELECT pattern matched: after comma, before FROM'); // Debug
|
|
autocompleteState.triggerPosition = cursorPosition - (commaMatch[1] ? commaMatch[1].length : 0);
|
|
return true;
|
|
}
|
|
|
|
console.log('No SELECT pattern matched'); // Debug
|
|
return false;
|
|
}
|
|
|
|
// Prüfe ob Tabellen-Autocomplete angezeigt werden soll
|
|
function shouldShowTableAutocomplete(text, cursorPosition) {
|
|
// Extrahiere Text bis zur aktuellen Cursor-Position
|
|
const textBeforeCursor = text.substring(0, cursorPosition).toUpperCase();
|
|
|
|
// Suche nach "FROM" gefolgt von optionalem Leerzeichen
|
|
const fromMatch = textBeforeCursor.match(/\bFROM\s*$/);
|
|
if (fromMatch) {
|
|
autocompleteState.triggerPosition = cursorPosition;
|
|
return true;
|
|
}
|
|
|
|
// Suche nach "FROM" gefolgt von angefangenem Tabellennamen
|
|
const partialMatch = textBeforeCursor.match(/\bFROM\s+([A-Z_0-9]*)$/);
|
|
if (partialMatch) {
|
|
autocompleteState.triggerPosition = cursorPosition - partialMatch[1].length;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Zeige Spalten-Autocomplete an
|
|
function showColumnAutocomplete(text, cursorPosition) {
|
|
const queryInput = document.getElementById('query-input');
|
|
const dropdown = document.getElementById('autocomplete-dropdown');
|
|
|
|
console.log('showColumnAutocomplete called for text:', text.substring(0, cursorPosition)); // Debug
|
|
|
|
// Prüfe ob wir eine FROM-Klausel haben
|
|
const textBeforeCursor = text.substring(0, cursorPosition).toUpperCase();
|
|
const hasFromClause = /\bFROM\s+[A-Z_][A-Z0-9_]*/.test(textBeforeCursor);
|
|
|
|
console.log('Has FROM clause:', hasFromClause); // Debug
|
|
|
|
if (hasFromClause) {
|
|
// Wenn FROM-Klausel vorhanden, zeige Spalten der spezifischen Tabelle
|
|
const tableName = extractTableNameFromQuery(text);
|
|
console.log('Extracted table name:', tableName); // Debug
|
|
if (tableName) {
|
|
console.log('Loading schema for specific table:', tableName); // Debug
|
|
loadTableSchemaForAutocomplete(tableName, text, cursorPosition);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// WICHTIG: Auch ohne FROM-Klausel alle verfügbaren Spalten anzeigen
|
|
console.log('Loading all columns for autocomplete (no FROM clause or no table found)'); // Debug
|
|
|
|
// Fallback: Verwende einfach die erste verfügbare Tabelle als Beispiel
|
|
if (currentTables && currentTables.length > 0) {
|
|
console.log('Using first table as fallback:', currentTables[0]); // Debug
|
|
loadTableSchemaForAutocomplete(currentTables[0], text, cursorPosition);
|
|
} else {
|
|
console.log('No tables available, showing generic'); // Debug
|
|
showGenericColumnAutocomplete(text, cursorPosition);
|
|
}
|
|
}
|
|
|
|
// Zeige allgemeine Spalten-Optionen
|
|
function showGenericColumnAutocomplete(text, cursorPosition) {
|
|
const dropdown = document.getElementById('autocomplete-dropdown');
|
|
|
|
// Filtere basierend auf bereits getipptem Text
|
|
const textBeforeCursor = text.substring(0, cursorPosition).toUpperCase();
|
|
const columnMatch = textBeforeCursor.match(/(?:SELECT\s+(?:[^,]+,\s*)*|,\s*)([A-Z_0-9]*)$/);
|
|
const searchTerm = columnMatch ? columnMatch[1] : '';
|
|
|
|
// Standard-Spalten-Optionen
|
|
const genericColumns = ['*', 'COUNT(*)', 'COUNT(1)', 'ROWNUM', 'SYSDATE'];
|
|
const filteredColumns = genericColumns.filter(col =>
|
|
col.includes(searchTerm)
|
|
);
|
|
|
|
if (filteredColumns.length === 0) {
|
|
hideAutocomplete();
|
|
return;
|
|
}
|
|
|
|
let dropdownHtml = '<div class="autocomplete-header">Allgemeine Spalten</div>';
|
|
|
|
filteredColumns.forEach((column, index) => {
|
|
dropdownHtml += `
|
|
<div class="autocomplete-item" data-index="${index}" data-column="${column}" onclick="selectColumnFromAutocomplete('${column}')">
|
|
<i class="fas fa-columns"></i>
|
|
${column}
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
dropdown.innerHTML = dropdownHtml;
|
|
dropdown.style.display = 'block';
|
|
|
|
autocompleteState.isVisible = true;
|
|
autocompleteState.selectedIndex = -1;
|
|
autocompleteState.items = filteredColumns;
|
|
autocompleteState.type = 'column';
|
|
}
|
|
|
|
// Lade alle verfügbaren Spalten für Autocomplete
|
|
async function loadAllColumnsForAutocomplete(text, cursorPosition) {
|
|
try {
|
|
console.log('loadAllColumnsForAutocomplete called'); // Debug
|
|
console.log('currentTables:', currentTables); // Debug
|
|
console.log('currentConnection:', currentConnection); // Debug
|
|
|
|
if (!currentTables || currentTables.length === 0) {
|
|
console.log('No tables available, showing generic'); // Debug
|
|
showGenericColumnAutocomplete(text, cursorPosition);
|
|
return;
|
|
}
|
|
|
|
console.log('Loading schemas for', currentTables.length, 'tables'); // Debug
|
|
|
|
// Sammle alle Spalten von allen Tabellen
|
|
const allColumns = new Map(); // Map für Spaltenname -> Liste von Tabellen
|
|
let loadedCount = 0;
|
|
const maxTables = 5; // Reduziere auf 5 Tabellen für bessere Performance
|
|
|
|
// Teste zuerst eine einzelne Tabelle
|
|
const testTable = currentTables[0];
|
|
console.log('Testing single table first:', testTable); // Debug
|
|
|
|
try {
|
|
const testResponse = await fetch(`/get_table_schema/${testTable}?connection=${currentConnection}`);
|
|
const testData = await testResponse.json();
|
|
console.log('Test response for', testTable, ':', testData); // Debug
|
|
|
|
if (!testData.success) {
|
|
console.error('Test table schema failed:', testData); // Debug
|
|
showGenericColumnAutocomplete(text, cursorPosition);
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
console.error('Test table request failed:', error); // Debug
|
|
showGenericColumnAutocomplete(text, cursorPosition);
|
|
return;
|
|
}
|
|
|
|
// Wenn Test erfolgreich, lade alle Tabellen
|
|
const promises = currentTables.slice(0, maxTables).map(async (tableName) => {
|
|
try {
|
|
console.log('Loading schema for:', tableName); // Debug
|
|
const response = await fetch(`/get_table_schema/${tableName}?connection=${currentConnection}`);
|
|
|
|
if (!response.ok) {
|
|
console.warn(`HTTP ${response.status} for table ${tableName}`); // Debug
|
|
return;
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log(`Schema response for ${tableName}:`, data); // Debug
|
|
|
|
if (data.success && data.columns && Array.isArray(data.columns)) {
|
|
console.log(`Found ${data.columns.length} columns in ${tableName}`); // Debug
|
|
data.columns.forEach(column => {
|
|
const columnName = column.name || column.COLUMN_NAME || column;
|
|
if (columnName && typeof columnName === 'string') {
|
|
if (!allColumns.has(columnName)) {
|
|
allColumns.set(columnName, []);
|
|
}
|
|
allColumns.get(columnName).push({
|
|
table: tableName,
|
|
type: column.type || column.DATA_TYPE || 'unknown'
|
|
});
|
|
}
|
|
});
|
|
} else {
|
|
console.warn(`No valid columns found for table ${tableName}:`, data); // Debug
|
|
}
|
|
loadedCount++;
|
|
} catch (error) {
|
|
console.warn(`Fehler beim Laden von Tabelle ${tableName}:`, error);
|
|
}
|
|
});
|
|
|
|
await Promise.all(promises);
|
|
|
|
console.log(`Loaded ${loadedCount} tables, found ${allColumns.size} unique columns`); // Debug
|
|
console.log('Sample columns:', Array.from(allColumns.keys()).slice(0, 10)); // Debug
|
|
|
|
if (allColumns.size > 0) {
|
|
showAllColumnsAutocomplete(allColumns, text, cursorPosition);
|
|
} else {
|
|
console.log('No columns found, falling back to generic'); // Debug
|
|
showGenericColumnAutocomplete(text, cursorPosition);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden aller Spalten:', error);
|
|
showGenericColumnAutocomplete(text, cursorPosition);
|
|
}
|
|
}
|
|
|
|
// Lade Schema für Autocomplete
|
|
async function loadTableSchemaForAutocomplete(tableName, text, cursorPosition) {
|
|
try {
|
|
console.log('Loading schema for table:', tableName); // Debug
|
|
|
|
// Versuche zuerst ohne connection Parameter
|
|
let url = `/get_table_schema/${tableName}`;
|
|
let response = await fetch(url);
|
|
|
|
// Falls das fehlschlägt, versuche mit connection Parameter
|
|
if (!response.ok && currentConnection) {
|
|
console.log('Trying with connection parameter:', currentConnection); // Debug
|
|
url = `/get_table_schema/${tableName}?connection=${currentConnection}`;
|
|
response = await fetch(url);
|
|
}
|
|
|
|
if (!response.ok) {
|
|
console.warn('HTTP Error:', response.status, response.statusText); // Debug
|
|
showGenericColumnAutocomplete(text, cursorPosition);
|
|
return;
|
|
}
|
|
|
|
// Prüfe ob es eine HTML-Antwort ist (Login-Redirect)
|
|
const contentType = response.headers.get('content-type');
|
|
if (contentType && contentType.includes('text/html')) {
|
|
console.warn('Received HTML response (probably login redirect)'); // Debug
|
|
showGenericColumnAutocomplete(text, cursorPosition);
|
|
return;
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log('Schema response for', tableName, ':', data); // Debug
|
|
|
|
// Prüfe verschiedene Schlüssel für Spalten-Daten
|
|
let columns = null;
|
|
if (data.success) {
|
|
columns = data.columns || data.schema || data.fields;
|
|
}
|
|
|
|
if (columns && Array.isArray(columns) && columns.length > 0) {
|
|
console.log('Found', columns.length, 'columns for', tableName); // Debug
|
|
|
|
// Normalisiere die Spalten-Daten (verschiedene Formate unterstützen)
|
|
const normalizedColumns = columns.map(col => {
|
|
if (typeof col === 'string') {
|
|
return { name: col, type: 'unknown' };
|
|
} else if (typeof col === 'object') {
|
|
return {
|
|
name: col.name || col.COLUMN_NAME || col.column_name || 'unknown',
|
|
type: col.type || col.DATA_TYPE || col.data_type || 'unknown'
|
|
};
|
|
}
|
|
return { name: 'unknown', type: 'unknown' };
|
|
});
|
|
|
|
console.log('Normalized columns sample:', normalizedColumns.slice(0, 3)); // Debug
|
|
showTableColumnsAutocomplete(normalizedColumns, text, cursorPosition, tableName);
|
|
} else {
|
|
console.warn('No valid columns found in response:', data); // Debug
|
|
showGenericColumnAutocomplete(text, cursorPosition);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading schema for', tableName, ':', error);
|
|
showGenericColumnAutocomplete(text, cursorPosition);
|
|
}
|
|
}
|
|
|
|
// Zeige alle verfügbaren Spalten im Autocomplete
|
|
function showAllColumnsAutocomplete(allColumns, text, cursorPosition) {
|
|
const dropdown = document.getElementById('autocomplete-dropdown');
|
|
|
|
console.log('showAllColumnsAutocomplete called with', allColumns.size, 'unique columns'); // Debug
|
|
|
|
// Filtere Spalten basierend auf bereits getipptem Text
|
|
const textBeforeCursor = text.substring(0, cursorPosition).toUpperCase();
|
|
const columnMatch = textBeforeCursor.match(/(?:SELECT\s+(?:[^,]+,\s*)*|,\s*)([A-Z_0-9]*)$/);
|
|
const searchTerm = columnMatch ? columnMatch[1] : '';
|
|
|
|
console.log('Search term:', searchTerm); // Debug
|
|
|
|
// Konvertiere Map zu Array und sortiere
|
|
const columnList = Array.from(allColumns.entries())
|
|
.filter(([columnName]) => columnName.toUpperCase().includes(searchTerm))
|
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
.slice(0, 20); // Limitiere auf 20 Einträge
|
|
|
|
console.log('Filtered to', columnList.length, 'columns'); // Debug
|
|
|
|
if (columnList.length === 0) {
|
|
console.log('No columns found, showing generic'); // Debug
|
|
showGenericColumnAutocomplete(text, cursorPosition);
|
|
return;
|
|
}
|
|
|
|
let dropdownHtml = '<div class="autocomplete-header">Alle verfügbaren Spalten</div>';
|
|
|
|
columnList.forEach(([columnName, tables], index) => {
|
|
const firstTable = tables[0];
|
|
const moreTablesCount = tables.length - 1;
|
|
const tableInfo = moreTablesCount > 0 ?
|
|
` (${firstTable.table} +${moreTablesCount} weitere)` :
|
|
` (${firstTable.table})`;
|
|
|
|
dropdownHtml += `
|
|
<div class="autocomplete-item" data-index="${index}" data-column="${columnName}" onclick="selectColumnFromAutocomplete('${columnName}')">
|
|
<i class="fas fa-columns"></i>
|
|
<strong>${columnName}</strong>
|
|
<small class="text-muted">${tableInfo}</small>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
dropdown.innerHTML = dropdownHtml;
|
|
dropdown.style.display = 'block';
|
|
|
|
autocompleteState.isVisible = true;
|
|
autocompleteState.selectedIndex = -1;
|
|
autocompleteState.items = columnList.map(([columnName]) => columnName);
|
|
autocompleteState.type = 'column';
|
|
|
|
console.log('Autocomplete dropdown shown with', columnList.length, 'items'); // Debug
|
|
}
|
|
|
|
// Zeige Tabellen-Spalten im Autocomplete
|
|
function showTableColumnsAutocomplete(columns, text, cursorPosition, tableName) {
|
|
const dropdown = document.getElementById('autocomplete-dropdown');
|
|
|
|
// Filtere Spalten basierend auf bereits getipptem Text
|
|
const textBeforeCursor = text.substring(0, cursorPosition).toUpperCase();
|
|
const columnMatch = textBeforeCursor.match(/(?:SELECT\s+(?:[^,]+,\s*)*|,\s*)([A-Z_0-9]*)$/);
|
|
const searchTerm = columnMatch ? columnMatch[1] : '';
|
|
|
|
// Füge * als erste Option hinzu
|
|
const allColumns = ['*', ...columns.map(col => col.name)];
|
|
const filteredColumns = allColumns.filter(col =>
|
|
col.toUpperCase().includes(searchTerm)
|
|
).slice(0, 15); // Limitiere auf 15 Einträge
|
|
|
|
if (filteredColumns.length === 0) {
|
|
hideAutocomplete();
|
|
return;
|
|
}
|
|
|
|
let dropdownHtml = `<div class="autocomplete-header">Spalten von ${tableName}</div>`;
|
|
|
|
filteredColumns.forEach((column, index) => {
|
|
const columnInfo = columns.find(col => col.name === column);
|
|
const icon = column === '*' ? 'fas fa-asterisk' : 'fas fa-columns';
|
|
const typeInfo = columnInfo ? ` (${columnInfo.type})` : '';
|
|
|
|
dropdownHtml += `
|
|
<div class="autocomplete-item" data-index="${index}" data-column="${column}" onclick="selectColumnFromAutocomplete('${column}')">
|
|
<i class="${icon}"></i>
|
|
${column}${typeInfo}
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
dropdown.innerHTML = dropdownHtml;
|
|
dropdown.style.display = 'block';
|
|
|
|
autocompleteState.isVisible = true;
|
|
autocompleteState.selectedIndex = -1;
|
|
autocompleteState.items = filteredColumns;
|
|
autocompleteState.type = 'column';
|
|
}
|
|
|
|
// Zeige Tabellen-Autocomplete an
|
|
function showTableAutocomplete(cursorPosition) {
|
|
const queryInput = document.getElementById('query-input');
|
|
const dropdown = document.getElementById('autocomplete-dropdown');
|
|
|
|
if (!currentTables || currentTables.length === 0) {
|
|
hideAutocomplete();
|
|
return;
|
|
}
|
|
|
|
// Filtere Tabellen basierend auf bereits getipptem Text
|
|
const textBeforeCursor = queryInput.value.substring(0, cursorPosition).toUpperCase();
|
|
const partialMatch = textBeforeCursor.match(/\bFROM\s+([A-Z_0-9]*)$/);
|
|
const searchTerm = partialMatch ? partialMatch[1] : '';
|
|
|
|
const filteredTables = currentTables.filter(table =>
|
|
table.toUpperCase().includes(searchTerm)
|
|
).slice(0, 10); // Limitiere auf 10 Einträge
|
|
|
|
if (filteredTables.length === 0) {
|
|
hideAutocomplete();
|
|
return;
|
|
}
|
|
|
|
// Erstelle Dropdown-Inhalt
|
|
let dropdownHtml = '<div class="autocomplete-header">Verfügbare Tabellen</div>';
|
|
|
|
filteredTables.forEach((table, index) => {
|
|
dropdownHtml += `
|
|
<div class="autocomplete-item" data-index="${index}" data-table="${table}" onclick="selectTableFromAutocomplete('${table}')">
|
|
<i class="fas fa-table"></i>
|
|
${table}
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
dropdown.innerHTML = dropdownHtml;
|
|
dropdown.style.display = 'block';
|
|
|
|
autocompleteState.isVisible = true;
|
|
autocompleteState.selectedIndex = -1;
|
|
autocompleteState.items = filteredTables;
|
|
autocompleteState.type = 'table';
|
|
}
|
|
|
|
// Extrahiere Tabellenname aus Query
|
|
function extractTableNameFromQuery(text) {
|
|
try {
|
|
const upperText = text.toUpperCase();
|
|
|
|
// Verschiedene Patterns für FROM-Erkennung
|
|
const patterns = [
|
|
/\bFROM\s+([A-Z_][A-Z0-9_]*)/, // Standard: FROM TABLE
|
|
/\bFROM\s+([A-Z_][A-Z0-9_]*)\s+[A-Z_]/, // Mit Alias: FROM TABLE ALIAS
|
|
/\bFROM\s+([A-Z_][A-Z0-9_]*)\s*,/, // Mit weiteren Tabellen: FROM TABLE,
|
|
/\bFROM\s+([A-Z_][A-Z0-9_]*)\s*$/, // Am Ende: FROM TABLE$
|
|
/\bFROM\s+([A-Z_][A-Z0-9_]*)\s+WHERE/, // Mit WHERE: FROM TABLE WHERE
|
|
/\bFROM\s+([A-Z_][A-Z0-9_]*)\s+ORDER/, // Mit ORDER: FROM TABLE ORDER
|
|
/\bFROM\s+([A-Z_][A-Z0-9_]*)\s+GROUP/ // Mit GROUP: FROM TABLE GROUP
|
|
];
|
|
|
|
for (const pattern of patterns) {
|
|
const match = upperText.match(pattern);
|
|
if (match && match[1]) {
|
|
return match[1];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
|
|
} catch (error) {
|
|
console.error('Error extracting table name:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Verstecke Autocomplete
|
|
function hideAutocomplete() {
|
|
const dropdown = document.getElementById('autocomplete-dropdown');
|
|
if (dropdown) {
|
|
dropdown.style.display = 'none';
|
|
}
|
|
autocompleteState.isVisible = false;
|
|
autocompleteState.selectedIndex = -1;
|
|
autocompleteState.items = [];
|
|
autocompleteState.type = null;
|
|
}
|
|
|
|
// Bewege Auswahl im Autocomplete
|
|
function moveAutocompleteSelection(direction) {
|
|
const dropdown = document.getElementById('autocomplete-dropdown');
|
|
const items = dropdown.querySelectorAll('.autocomplete-item');
|
|
|
|
if (items.length === 0) return;
|
|
|
|
// Entferne vorherige Auswahl
|
|
items.forEach(item => item.classList.remove('selected'));
|
|
|
|
// Berechne neue Position
|
|
autocompleteState.selectedIndex += direction;
|
|
|
|
if (autocompleteState.selectedIndex < 0) {
|
|
autocompleteState.selectedIndex = items.length - 1;
|
|
} else if (autocompleteState.selectedIndex >= items.length) {
|
|
autocompleteState.selectedIndex = 0;
|
|
}
|
|
|
|
// Markiere neue Auswahl
|
|
items[autocompleteState.selectedIndex].classList.add('selected');
|
|
items[autocompleteState.selectedIndex].scrollIntoView({ block: 'nearest' });
|
|
}
|
|
|
|
// Wähle Autocomplete-Item aus
|
|
function selectAutocompleteItem() {
|
|
if (autocompleteState.selectedIndex >= 0 && autocompleteState.selectedIndex < autocompleteState.items.length) {
|
|
const selectedItem = autocompleteState.items[autocompleteState.selectedIndex];
|
|
|
|
if (autocompleteState.type === 'column') {
|
|
selectColumnFromAutocomplete(selectedItem);
|
|
} else {
|
|
selectTableFromAutocomplete(selectedItem);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Wähle Spalte aus Autocomplete aus
|
|
function selectColumnFromAutocomplete(columnName) {
|
|
const queryInput = document.getElementById('query-input');
|
|
const cursorPosition = queryInput.selectionStart;
|
|
const text = queryInput.value;
|
|
const textBeforeCursor = text.substring(0, cursorPosition).toUpperCase();
|
|
|
|
// Finde Position für Spalten-Einfügung
|
|
let insertPosition = cursorPosition;
|
|
let replaceLength = 0;
|
|
|
|
// Prüfe verschiedene SELECT-Muster
|
|
const selectMatch = textBeforeCursor.match(/\bSELECT\s*$/);
|
|
const columnMatch = textBeforeCursor.match(/\bSELECT\s+(?:[^,]+,\s*)*([A-Z_0-9]*)$/);
|
|
const commaMatch = textBeforeCursor.match(/\bSELECT\s+[^,]+(?:\s*,\s*[^,]*)*\s*,\s*([A-Z_0-9]*)$/);
|
|
|
|
if (selectMatch) {
|
|
// Direkt nach SELECT
|
|
insertPosition = cursorPosition;
|
|
} else if (commaMatch) {
|
|
// Nach Komma in SELECT
|
|
const partialColumn = commaMatch[1];
|
|
insertPosition = cursorPosition - partialColumn.length;
|
|
replaceLength = partialColumn.length;
|
|
} else if (columnMatch) {
|
|
// Erste Spalte nach SELECT
|
|
const partialColumn = columnMatch[1];
|
|
insertPosition = cursorPosition - partialColumn.length;
|
|
replaceLength = partialColumn.length;
|
|
}
|
|
|
|
// Füge Spalte ein
|
|
const beforeInsert = text.substring(0, insertPosition);
|
|
const afterInsert = text.substring(insertPosition + replaceLength);
|
|
|
|
const newText = beforeInsert + columnName + afterInsert;
|
|
const newCursorPosition = insertPosition + columnName.length;
|
|
|
|
queryInput.value = newText;
|
|
queryInput.setSelectionRange(newCursorPosition, newCursorPosition);
|
|
queryInput.focus();
|
|
|
|
hideAutocomplete();
|
|
}
|
|
|
|
// Wähle Tabelle aus Autocomplete aus
|
|
function selectTableFromAutocomplete(tableName) {
|
|
const queryInput = document.getElementById('query-input');
|
|
const cursorPosition = queryInput.selectionStart;
|
|
const text = queryInput.value;
|
|
|
|
// Finde die Position wo "FROM" beginnt
|
|
const textBeforeCursor = text.substring(0, cursorPosition).toUpperCase();
|
|
const fromMatch = textBeforeCursor.match(/\bFROM\s*/);
|
|
|
|
if (fromMatch) {
|
|
const fromStartPosition = textBeforeCursor.lastIndexOf(fromMatch[0].trim());
|
|
const fromEndPosition = fromStartPosition + fromMatch[0].length;
|
|
|
|
// Ersetze den Text nach "FROM " mit der ausgewählten Tabelle
|
|
const beforeFrom = text.substring(0, fromEndPosition);
|
|
const afterCursor = text.substring(cursorPosition);
|
|
|
|
const newText = beforeFrom + tableName + ' ' + afterCursor;
|
|
const newCursorPosition = fromEndPosition + tableName.length + 1;
|
|
|
|
queryInput.value = newText;
|
|
queryInput.setSelectionRange(newCursorPosition, newCursorPosition);
|
|
queryInput.focus();
|
|
}
|
|
|
|
hideAutocomplete();
|
|
}
|
|
|
|
// Initialisiere Autocomplete beim Laden der Seite
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
initializeAutocomplete();
|
|
}); |