first commit
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import LoginManager
|
||||
from flask_cors import CORS
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Lade Umgebungsvariablen
|
||||
load_dotenv()
|
||||
|
||||
db = SQLAlchemy()
|
||||
login_manager = LoginManager()
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
|
||||
# Konfiguration
|
||||
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY') or 'dev-secret-key'
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL') or 'sqlite:///querybuilder.db'
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
# Initialisiere Extensions
|
||||
db.init_app(app)
|
||||
login_manager.init_app(app)
|
||||
login_manager.login_view = 'auth.login'
|
||||
login_manager.login_message = 'Bitte melden Sie sich an, um auf diese Seite zuzugreifen.'
|
||||
|
||||
CORS(app)
|
||||
|
||||
# Registriere Blueprints
|
||||
from app.routes.auth import auth_bp
|
||||
from app.routes.main import main_bp
|
||||
from app.routes.api import api_bp
|
||||
from app.routes.admin import admin_bp
|
||||
|
||||
app.register_blueprint(auth_bp, url_prefix='/auth')
|
||||
app.register_blueprint(main_bp)
|
||||
app.register_blueprint(api_bp, url_prefix='/api')
|
||||
app.register_blueprint(admin_bp, url_prefix='/admin')
|
||||
|
||||
# Erstelle Datenbanktabellen
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
# Erstelle Standard-Admin-User falls nicht vorhanden
|
||||
from app.models import User
|
||||
admin = User.query.filter_by(username='admin').first()
|
||||
if not admin:
|
||||
admin = User(username='admin', email='admin@example.com')
|
||||
admin.set_password('admin123')
|
||||
db.session.add(admin)
|
||||
db.session.commit()
|
||||
|
||||
return app
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
from app.models import User
|
||||
return User.query.get(int(user_id))
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,73 @@
|
||||
from app import db
|
||||
from flask_login import UserMixin
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||
password_hash = db.Column(db.String(128))
|
||||
is_admin = db.Column(db.Boolean, default=False, nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
# Beziehungen
|
||||
saved_queries = db.relationship('SavedQuery', backref='user', lazy=True, cascade='all, delete-orphan')
|
||||
|
||||
def set_password(self, password):
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<User {self.username}>'
|
||||
|
||||
class SavedQuery(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(200), nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
query_text = db.Column('query', db.Text, nullable=False) # Expliziter Spaltenname 'query', Attribut 'query_text'
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'query': self.query_text,
|
||||
'created_at': self.created_at.isoformat(),
|
||||
'updated_at': self.updated_at.isoformat()
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return f'<SavedQuery {self.name}>'
|
||||
|
||||
class DatabaseConnection(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
host = db.Column(db.String(200))
|
||||
port = db.Column(db.Integer)
|
||||
database = db.Column(db.String(100))
|
||||
username = db.Column(db.String(100))
|
||||
password = db.Column(db.String(200)) # In Produktion sollte dies verschlüsselt werden
|
||||
db_type = db.Column(db.String(50)) # mysql, postgresql, sqlite, etc.
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'host': self.host,
|
||||
'port': self.port,
|
||||
'database': self.database,
|
||||
'username': self.username,
|
||||
'db_type': self.db_type
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return f'<DatabaseConnection {self.name}>'
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,187 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
|
||||
from flask_login import login_required, current_user
|
||||
from functools import wraps
|
||||
from app.models import User, db
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||
|
||||
def admin_required(f):
|
||||
@wraps(f)
|
||||
@login_required
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not current_user.is_admin:
|
||||
flash('Zugriff verweigert. Administrator-Rechte erforderlich.', 'error')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
@admin_bp.route('/')
|
||||
@admin_required
|
||||
def admin_dashboard():
|
||||
"""Admin Dashboard mit Benutzerübersicht"""
|
||||
users = User.query.order_by(User.created_at.desc()).all()
|
||||
return render_template('admin/dashboard.html', users=users)
|
||||
|
||||
@admin_bp.route('/users')
|
||||
@admin_required
|
||||
def users():
|
||||
"""Benutzerverwaltung"""
|
||||
users = User.query.order_by(User.created_at.desc()).all()
|
||||
return render_template('admin/users.html', users=users)
|
||||
|
||||
@admin_bp.route('/users/create', methods=['GET', 'POST'])
|
||||
@admin_required
|
||||
def create_user():
|
||||
"""Neuen Benutzer erstellen"""
|
||||
if request.method == 'POST':
|
||||
username = request.form.get('username', '').strip()
|
||||
email = request.form.get('email', '').strip()
|
||||
password = request.form.get('password', '').strip()
|
||||
is_admin = 'is_admin' in request.form
|
||||
|
||||
# Validierung
|
||||
if not username or not email or not password:
|
||||
flash('Alle Felder sind erforderlich.', 'error')
|
||||
return render_template('admin/create_user.html')
|
||||
|
||||
# Prüfe ob Benutzername bereits existiert
|
||||
if User.query.filter_by(username=username).first():
|
||||
flash('Benutzername bereits vergeben.', 'error')
|
||||
return render_template('admin/create_user.html')
|
||||
|
||||
# Prüfe ob E-Mail bereits existiert
|
||||
if User.query.filter_by(email=email).first():
|
||||
flash('E-Mail-Adresse bereits vergeben.', 'error')
|
||||
return render_template('admin/create_user.html')
|
||||
|
||||
try:
|
||||
# Benutzer erstellen
|
||||
user = User(
|
||||
username=username,
|
||||
email=email,
|
||||
is_admin=is_admin
|
||||
)
|
||||
user.set_password(password)
|
||||
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
flash(f'Benutzer "{username}" erfolgreich erstellt.', 'success')
|
||||
return redirect(url_for('admin.users'))
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash(f'Fehler beim Erstellen des Benutzers: {str(e)}', 'error')
|
||||
|
||||
return render_template('admin/create_user.html')
|
||||
|
||||
@admin_bp.route('/users/<int:user_id>/edit', methods=['GET', 'POST'])
|
||||
@admin_required
|
||||
def edit_user(user_id):
|
||||
"""Benutzer bearbeiten"""
|
||||
user = User.query.get_or_404(user_id)
|
||||
|
||||
if request.method == 'POST':
|
||||
username = request.form.get('username', '').strip()
|
||||
email = request.form.get('email', '').strip()
|
||||
password = request.form.get('password', '').strip()
|
||||
is_admin = 'is_admin' in request.form
|
||||
|
||||
# Validierung
|
||||
if not username or not email:
|
||||
flash('Benutzername und E-Mail sind erforderlich.', 'error')
|
||||
return render_template('admin/edit_user.html', user=user)
|
||||
|
||||
# Prüfe ob Benutzername bereits von anderem User verwendet wird
|
||||
existing_user = User.query.filter_by(username=username).first()
|
||||
if existing_user and existing_user.id != user_id:
|
||||
flash('Benutzername bereits vergeben.', 'error')
|
||||
return render_template('admin/edit_user.html', user=user)
|
||||
|
||||
# Prüfe ob E-Mail bereits von anderem User verwendet wird
|
||||
existing_email = User.query.filter_by(email=email).first()
|
||||
if existing_email and existing_email.id != user_id:
|
||||
flash('E-Mail-Adresse bereits vergeben.', 'error')
|
||||
return render_template('admin/edit_user.html', user=user)
|
||||
|
||||
try:
|
||||
# Benutzer aktualisieren
|
||||
user.username = username
|
||||
user.email = email
|
||||
user.is_admin = is_admin
|
||||
|
||||
# Passwort nur ändern wenn angegeben
|
||||
if password:
|
||||
user.set_password(password)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
flash(f'Benutzer "{username}" erfolgreich aktualisiert.', 'success')
|
||||
return redirect(url_for('admin.users'))
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash(f'Fehler beim Aktualisieren des Benutzers: {str(e)}', 'error')
|
||||
|
||||
return render_template('admin/edit_user.html', user=user)
|
||||
|
||||
@admin_bp.route('/users/<int:user_id>/delete', methods=['POST'])
|
||||
@admin_required
|
||||
def delete_user(user_id):
|
||||
"""Benutzer löschen"""
|
||||
user = User.query.get_or_404(user_id)
|
||||
|
||||
# Verhindere dass der Admin sich selbst löscht
|
||||
if user.id == current_user.id:
|
||||
flash('Sie können sich nicht selbst löschen.', 'error')
|
||||
return redirect(url_for('admin.users'))
|
||||
|
||||
# Verhindere das Löschen des letzten Admins
|
||||
if user.is_admin:
|
||||
admin_count = User.query.filter_by(is_admin=True).count()
|
||||
if admin_count <= 1:
|
||||
flash('Der letzte Administrator kann nicht gelöscht werden.', 'error')
|
||||
return redirect(url_for('admin.users'))
|
||||
|
||||
try:
|
||||
username = user.username
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
|
||||
flash(f'Benutzer "{username}" erfolgreich gelöscht.', 'success')
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash(f'Fehler beim Löschen des Benutzers: {str(e)}', 'error')
|
||||
|
||||
return redirect(url_for('admin.users'))
|
||||
|
||||
@admin_bp.route('/users/<int:user_id>/toggle_admin', methods=['POST'])
|
||||
@admin_required
|
||||
def toggle_admin(user_id):
|
||||
"""Admin-Status umschalten"""
|
||||
user = User.query.get_or_404(user_id)
|
||||
|
||||
# Verhindere dass der Admin sich selbst die Admin-Rechte entzieht
|
||||
if user.id == current_user.id and user.is_admin:
|
||||
return jsonify({'error': 'Sie können sich nicht selbst die Admin-Rechte entziehen.'}), 400
|
||||
|
||||
# Verhindere das Entziehen der Admin-Rechte des letzten Admins
|
||||
if user.is_admin:
|
||||
admin_count = User.query.filter_by(is_admin=True).count()
|
||||
if admin_count <= 1:
|
||||
return jsonify({'error': 'Der letzte Administrator kann nicht degradiert werden.'}), 400
|
||||
|
||||
try:
|
||||
user.is_admin = not user.is_admin
|
||||
db.session.commit()
|
||||
|
||||
status = 'Administrator' if user.is_admin else 'Benutzer'
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'{user.username} ist jetzt {status}.',
|
||||
'is_admin': user.is_admin
|
||||
})
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'error': str(e)}), 500
|
||||
@@ -0,0 +1,186 @@
|
||||
from flask import Blueprint, jsonify, request, make_response
|
||||
from flask_login import login_required, current_user
|
||||
from app.models import SavedQuery, db
|
||||
from app.services.database_service import DatabaseService
|
||||
from app.services.database_manager import DatabaseManager
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
|
||||
api_bp = Blueprint('api', __name__)
|
||||
|
||||
@api_bp.route('/queries', methods=['GET'])
|
||||
@login_required
|
||||
def get_saved_queries():
|
||||
"""API-Endpunkt um alle gespeicherten Queries zu erhalten"""
|
||||
try:
|
||||
queries = SavedQuery.query.filter_by(user_id=current_user.id).all()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'queries': [query.to_dict() for query in queries]
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@api_bp.route('/queries/<query_name>', methods=['GET'])
|
||||
@login_required
|
||||
def get_query_by_name(query_name):
|
||||
"""API-Endpunkt um eine gespeicherte Query nach Namen zu erhalten"""
|
||||
try:
|
||||
query = SavedQuery.query.filter_by(name=query_name, user_id=current_user.id).first()
|
||||
if not query:
|
||||
return jsonify({'error': 'Query nicht gefunden'}), 404
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'query': query.to_dict()
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@api_bp.route('/queries/<query_name>/execute', methods=['GET'])
|
||||
@login_required
|
||||
def execute_saved_query(query_name):
|
||||
"""API-Endpunkt um eine gespeicherte Query auszuführen und Ergebnisse zu erhalten"""
|
||||
try:
|
||||
# Finde gespeicherte Query
|
||||
saved_query = SavedQuery.query.filter_by(name=query_name, user_id=current_user.id).first()
|
||||
if not saved_query:
|
||||
return jsonify({'error': 'Query nicht gefunden'}), 404
|
||||
|
||||
# Datenbankverbindung bestimmen
|
||||
connection = request.args.get('connection', 'oracle')
|
||||
|
||||
# Führe Query aus
|
||||
db_manager = DatabaseManager()
|
||||
db_service = db_manager.get_database_service(connection)
|
||||
results = db_service.execute_query(saved_query.query_text)
|
||||
|
||||
# Format bestimmen (JSON oder CSV)
|
||||
format_type = request.args.get('format', 'json').lower()
|
||||
|
||||
if format_type == 'csv':
|
||||
return export_results_as_csv(results, query_name)
|
||||
else:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'query_name': query_name,
|
||||
'results': results
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@api_bp.route('/queries/<query_name>/export/csv', methods=['GET'])
|
||||
@login_required
|
||||
def export_query_results_csv(query_name):
|
||||
"""Exportiere Query-Ergebnisse als CSV"""
|
||||
try:
|
||||
# Finde gespeicherte Query
|
||||
saved_query = SavedQuery.query.filter_by(name=query_name, user_id=current_user.id).first()
|
||||
if not saved_query:
|
||||
return jsonify({'error': 'Query nicht gefunden'}), 404
|
||||
|
||||
# Datenbankverbindung bestimmen
|
||||
connection = request.args.get('connection', 'oracle')
|
||||
|
||||
# Führe Query aus
|
||||
db_manager = DatabaseManager()
|
||||
db_service = db_manager.get_database_service(connection)
|
||||
results = db_service.execute_query(saved_query.query_text)
|
||||
|
||||
return export_results_as_csv(results, query_name)
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@api_bp.route('/queries/<query_name>/export/json', methods=['GET'])
|
||||
@login_required
|
||||
def export_query_results_json(query_name):
|
||||
"""Exportiere Query-Ergebnisse als JSON"""
|
||||
try:
|
||||
# Finde gespeicherte Query
|
||||
saved_query = SavedQuery.query.filter_by(name=query_name, user_id=current_user.id).first()
|
||||
if not saved_query:
|
||||
return jsonify({'error': 'Query nicht gefunden'}), 404
|
||||
|
||||
# Datenbankverbindung bestimmen
|
||||
connection = request.args.get('connection', 'oracle')
|
||||
|
||||
# Führe Query aus
|
||||
db_manager = DatabaseManager()
|
||||
db_service = db_manager.get_database_service(connection)
|
||||
results = db_service.execute_query(saved_query.query_text)
|
||||
|
||||
# Erstelle JSON-Response mit Download-Header
|
||||
response = make_response(json.dumps({
|
||||
'query_name': query_name,
|
||||
'results': results
|
||||
}, indent=2))
|
||||
|
||||
response.headers['Content-Type'] = 'application/json'
|
||||
response.headers['Content-Disposition'] = f'attachment; filename="{query_name}_results.json"'
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@api_bp.route('/update_query_name/<int:query_id>', methods=['PUT'])
|
||||
@login_required
|
||||
def update_query_name(query_id):
|
||||
"""API-Endpunkt um den Namen einer gespeicherten Query zu aktualisieren"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
new_name = data.get('name', '').strip()
|
||||
|
||||
if not new_name:
|
||||
return jsonify({'error': 'Name ist erforderlich'}), 400
|
||||
|
||||
# Finde Query
|
||||
query = SavedQuery.query.filter_by(id=query_id, user_id=current_user.id).first()
|
||||
if not query:
|
||||
return jsonify({'error': 'Query nicht gefunden'}), 404
|
||||
|
||||
# Prüfe ob Name bereits existiert
|
||||
existing_query = SavedQuery.query.filter_by(name=new_name, user_id=current_user.id).first()
|
||||
if existing_query and existing_query.id != query_id:
|
||||
return jsonify({'error': 'Eine Query mit diesem Namen existiert bereits'}), 400
|
||||
|
||||
# Update Name
|
||||
query.name = new_name
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Query-Name erfolgreich aktualisiert',
|
||||
'query': query.to_dict()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
def export_results_as_csv(results, filename):
|
||||
"""Hilfsfunktion um Ergebnisse als CSV zu exportieren"""
|
||||
if not results or 'data' not in results or not results['data']:
|
||||
return jsonify({'error': 'Keine Daten zum Exportieren'}), 400
|
||||
|
||||
# Erstelle CSV
|
||||
output = io.StringIO()
|
||||
|
||||
# Headers schreiben
|
||||
if results['columns']:
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(results['columns'])
|
||||
|
||||
# Daten schreiben
|
||||
for row in results['data']:
|
||||
writer.writerow(row)
|
||||
|
||||
# Response erstellen
|
||||
response = make_response(output.getvalue())
|
||||
response.headers['Content-Type'] = 'text/csv'
|
||||
response.headers['Content-Disposition'] = f'attachment; filename="{filename}_results.csv"'
|
||||
|
||||
return response
|
||||
@@ -0,0 +1,56 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
||||
from flask_login import login_user, logout_user, login_required, current_user
|
||||
from app.models import User
|
||||
from app import db
|
||||
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
|
||||
@auth_bp.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
username = request.form['username']
|
||||
password = request.form['password']
|
||||
|
||||
user = User.query.filter_by(username=username).first()
|
||||
|
||||
if user and user.check_password(password):
|
||||
login_user(user)
|
||||
next_page = request.args.get('next')
|
||||
return redirect(next_page) if next_page else redirect(url_for('main.dashboard'))
|
||||
else:
|
||||
flash('Ungültiger Benutzername oder Passwort')
|
||||
|
||||
return render_template('auth/login.html')
|
||||
|
||||
@auth_bp.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
if request.method == 'POST':
|
||||
username = request.form['username']
|
||||
email = request.form['email']
|
||||
password = request.form['password']
|
||||
|
||||
# Prüfe ob Benutzer bereits existiert
|
||||
if User.query.filter_by(username=username).first():
|
||||
flash('Benutzername bereits vergeben')
|
||||
return render_template('auth/register.html')
|
||||
|
||||
if User.query.filter_by(email=email).first():
|
||||
flash('E-Mail bereits vergeben')
|
||||
return render_template('auth/register.html')
|
||||
|
||||
# Erstelle neuen Benutzer
|
||||
user = User(username=username, email=email)
|
||||
user.set_password(password)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
flash('Registrierung erfolgreich')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
return render_template('auth/register.html')
|
||||
|
||||
@auth_bp.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
return redirect(url_for('auth.login'))
|
||||
@@ -0,0 +1,182 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
|
||||
from flask_login import login_required, current_user
|
||||
from app.models import SavedQuery, DatabaseConnection
|
||||
from app.services.database_service import DatabaseService
|
||||
from app.services.database_manager import DatabaseManager
|
||||
from app import db
|
||||
import traceback
|
||||
|
||||
main_bp = Blueprint('main', __name__)
|
||||
|
||||
@main_bp.route('/')
|
||||
@login_required
|
||||
def dashboard():
|
||||
# Lade gespeicherte Queries des Benutzers
|
||||
saved_queries = SavedQuery.query.filter_by(user_id=current_user.id).all()
|
||||
|
||||
# Lade verfügbare Datenbankverbindungen
|
||||
db_manager = DatabaseManager()
|
||||
available_connections = db_manager.get_available_connections()
|
||||
|
||||
return render_template('dashboard.html',
|
||||
saved_queries=saved_queries,
|
||||
available_connections=available_connections)
|
||||
|
||||
@main_bp.route('/execute_query', methods=['POST'])
|
||||
@login_required
|
||||
def execute_query():
|
||||
data = request.get_json()
|
||||
query = data.get('query', '').strip()
|
||||
connection = data.get('connection', 'oracle') # Standard: Oracle
|
||||
|
||||
if not query:
|
||||
return jsonify({'error': 'Query ist leer'}), 400
|
||||
|
||||
try:
|
||||
# Verwende ausgewählte Datenbankverbindung
|
||||
db_manager = DatabaseManager()
|
||||
db_service = db_manager.get_database_service(connection)
|
||||
results = db_service.execute_query(query)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'results': results,
|
||||
'connection': connection
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'error': f'Fehler beim Ausführen der Query: {str(e)}',
|
||||
'traceback': traceback.format_exc()
|
||||
}), 500
|
||||
|
||||
@main_bp.route('/get_tables')
|
||||
@login_required
|
||||
def get_tables():
|
||||
connection = request.args.get('connection', 'oracle')
|
||||
|
||||
try:
|
||||
db_manager = DatabaseManager()
|
||||
db_service = db_manager.get_database_service(connection)
|
||||
tables = db_service.get_tables()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'tables': tables,
|
||||
'connection': connection
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'error': f'Fehler beim Laden der Tabellen: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@main_bp.route('/get_table_schema/<table_name>')
|
||||
@login_required
|
||||
def get_table_schema(table_name):
|
||||
connection = request.args.get('connection', 'oracle')
|
||||
|
||||
try:
|
||||
db_manager = DatabaseManager()
|
||||
db_service = db_manager.get_database_service(connection)
|
||||
schema = db_service.get_table_schema(table_name)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'schema': schema,
|
||||
'connection': connection
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'error': f'Fehler beim Laden des Tabellenschemas: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@main_bp.route('/save_query', methods=['POST'])
|
||||
@login_required
|
||||
def save_query():
|
||||
data = request.get_json()
|
||||
name = data.get('name', '').strip()
|
||||
description = data.get('description', '').strip()
|
||||
query = data.get('query', '').strip()
|
||||
|
||||
if not name or not query:
|
||||
return jsonify({'error': 'Name und Query sind erforderlich'}), 400
|
||||
|
||||
# Prüfe ob Query-Name bereits existiert
|
||||
existing = SavedQuery.query.filter_by(name=name, user_id=current_user.id).first()
|
||||
if existing:
|
||||
return jsonify({'error': 'Query-Name bereits vergeben'}), 400
|
||||
|
||||
try:
|
||||
saved_query = SavedQuery(
|
||||
name=name,
|
||||
description=description,
|
||||
query_text=query,
|
||||
user_id=current_user.id
|
||||
)
|
||||
|
||||
db.session.add(saved_query)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Query erfolgreich gespeichert',
|
||||
'query': saved_query.to_dict()
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'error': f'Fehler beim Speichern: {str(e)}'}), 500
|
||||
|
||||
@main_bp.route('/delete_query/<int:query_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_query(query_id):
|
||||
try:
|
||||
query = SavedQuery.query.filter_by(id=query_id, user_id=current_user.id).first()
|
||||
if not query:
|
||||
return jsonify({'error': 'Query nicht gefunden'}), 404
|
||||
|
||||
db.session.delete(query)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Query erfolgreich gelöscht'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'error': f'Fehler beim Löschen: {str(e)}'}), 500
|
||||
|
||||
@main_bp.route('/test_connection/<connection_name>')
|
||||
@login_required
|
||||
def test_connection(connection_name):
|
||||
try:
|
||||
db_manager = DatabaseManager()
|
||||
result = db_manager.test_connection(connection_name)
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': f'Fehler beim Testen der Verbindung: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@main_bp.route('/get_connections')
|
||||
@login_required
|
||||
def get_connections():
|
||||
try:
|
||||
db_manager = DatabaseManager()
|
||||
connections = db_manager.get_available_connections()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'connections': connections
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'error': f'Fehler beim Laden der Verbindungen: {str(e)}'
|
||||
}), 500
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,76 @@
|
||||
from app.services.database_service import DatabaseService
|
||||
import os
|
||||
|
||||
class DatabaseManager:
|
||||
"""Manager für verschiedene Datenbankverbindungen"""
|
||||
|
||||
def __init__(self):
|
||||
self.connections = {}
|
||||
self._load_default_connections()
|
||||
|
||||
def _load_default_connections(self):
|
||||
"""Lade Standard-Datenbankverbindungen"""
|
||||
|
||||
# Oracle-Datenbank (wenn konfiguriert)
|
||||
oracle_host = os.environ.get('ORACLE_HOST')
|
||||
if oracle_host:
|
||||
self.connections['oracle'] = {
|
||||
'name': 'Oracle Database',
|
||||
'type': 'oracle',
|
||||
'description': 'Oracle-Produktionsdatenbank',
|
||||
'host': oracle_host,
|
||||
'port': int(os.environ.get('ORACLE_PORT', 1521)),
|
||||
'service_name': os.environ.get('ORACLE_SERVICE_NAME', 'ORCL'),
|
||||
'username': os.environ.get('ORACLE_USERNAME'),
|
||||
'password': os.environ.get('ORACLE_PASSWORD')
|
||||
}
|
||||
|
||||
# PostgreSQL (wenn konfiguriert)
|
||||
postgres_host = os.environ.get('POSTGRES_HOST')
|
||||
if postgres_host:
|
||||
self.connections['postgres'] = {
|
||||
'name': 'PostgreSQL Database',
|
||||
'type': 'postgresql',
|
||||
'description': 'PostgreSQL-Datenbank',
|
||||
'host': postgres_host,
|
||||
'port': int(os.environ.get('POSTGRES_PORT', 5432)),
|
||||
'database': os.environ.get('POSTGRES_DATABASE', 'postgres'),
|
||||
'username': os.environ.get('POSTGRES_USERNAME'),
|
||||
'password': os.environ.get('POSTGRES_PASSWORD')
|
||||
}
|
||||
|
||||
def get_database_service(self, connection_name='demo'):
|
||||
"""Erstelle DatabaseService für spezifische Verbindung"""
|
||||
if connection_name not in self.connections:
|
||||
raise ValueError(f"Datenbankverbindung '{connection_name}' nicht gefunden")
|
||||
|
||||
config = self.connections[connection_name]
|
||||
|
||||
# Nur Oracle-Verbindungen unterstützt
|
||||
return DatabaseService(config)
|
||||
|
||||
def get_available_connections(self):
|
||||
"""Hole alle verfügbaren Datenbankverbindungen"""
|
||||
return [
|
||||
{
|
||||
'key': key,
|
||||
'name': config['name'],
|
||||
'type': config['type'],
|
||||
'description': config['description']
|
||||
}
|
||||
for key, config in self.connections.items()
|
||||
]
|
||||
|
||||
def test_connection(self, connection_name):
|
||||
"""Teste eine Datenbankverbindung"""
|
||||
try:
|
||||
db_service = self.get_database_service(connection_name)
|
||||
|
||||
# Oracle Test-Query
|
||||
test_query = "SELECT 1 FROM DUAL"
|
||||
|
||||
result = db_service.execute_query(test_query)
|
||||
return {'success': True, 'message': 'Verbindung erfolgreich'}
|
||||
|
||||
except Exception as e:
|
||||
return {'success': False, 'message': str(e)}
|
||||
@@ -0,0 +1,268 @@
|
||||
import os
|
||||
from flask import current_app
|
||||
import logging
|
||||
|
||||
# Oracle-Unterstützung
|
||||
try:
|
||||
import oracledb
|
||||
ORACLE_AVAILABLE = True
|
||||
except ImportError:
|
||||
try:
|
||||
import cx_Oracle
|
||||
ORACLE_AVAILABLE = True
|
||||
except ImportError:
|
||||
ORACLE_AVAILABLE = False
|
||||
logging.warning("Oracle-Treiber nicht verfügbar. Installiere cx-Oracle oder oracledb für Oracle-Unterstützung.")
|
||||
|
||||
# Weitere Datenbank-Treiber
|
||||
try:
|
||||
import psycopg2
|
||||
POSTGRESQL_AVAILABLE = True
|
||||
except ImportError:
|
||||
POSTGRESQL_AVAILABLE = False
|
||||
|
||||
try:
|
||||
import pymysql
|
||||
MYSQL_AVAILABLE = True
|
||||
except ImportError:
|
||||
MYSQL_AVAILABLE = False
|
||||
|
||||
class DatabaseService:
|
||||
def __init__(self, connection_config=None):
|
||||
"""
|
||||
Initialisiere Database Service - Nur Oracle-Unterstützung
|
||||
connection_config: Dictionary mit Oracle-Verbindungsparametern
|
||||
"""
|
||||
if connection_config is None:
|
||||
raise ValueError("Oracle-Verbindungskonfiguration erforderlich")
|
||||
|
||||
self.connection_config = connection_config
|
||||
self.db_type = connection_config.get('type', 'oracle')
|
||||
|
||||
if self.db_type == 'oracle':
|
||||
self._validate_oracle_config()
|
||||
else:
|
||||
raise ValueError(f"Datenbanktyp '{self.db_type}' wird nicht unterstützt. Nur Oracle ist verfügbar.")
|
||||
|
||||
def _validate_oracle_config(self):
|
||||
"""Validiere Oracle-Konfiguration"""
|
||||
if not ORACLE_AVAILABLE:
|
||||
raise Exception("Oracle-Treiber nicht verfügbar. Installiere cx-Oracle oder oracledb.")
|
||||
|
||||
required_fields = ['host', 'username', 'password']
|
||||
for field in required_fields:
|
||||
if not self.connection_config.get(field):
|
||||
raise Exception(f"Oracle-Konfiguration unvollständig: {field} fehlt")
|
||||
|
||||
def execute_query(self, query):
|
||||
"""Führe Oracle SQL-Query aus und gebe Ergebnisse zurück"""
|
||||
return self._execute_oracle_query(query)
|
||||
|
||||
def _execute_oracle_query(self, query):
|
||||
"""Führe Oracle Query aus"""
|
||||
if not ORACLE_AVAILABLE:
|
||||
raise Exception("Oracle-Treiber nicht verfügbar")
|
||||
|
||||
# Erstelle Oracle-Verbindung
|
||||
config = self.connection_config
|
||||
dsn = f"{config['host']}:{config.get('port', 1521)}/{config.get('service_name', 'ORCL')}"
|
||||
|
||||
try:
|
||||
# Verwende den neueren oracledb-Treiber wenn verfügbar, sonst cx_Oracle
|
||||
try:
|
||||
import oracledb
|
||||
conn = oracledb.connect(
|
||||
user=config['username'],
|
||||
password=config['password'],
|
||||
dsn=dsn
|
||||
)
|
||||
except ImportError:
|
||||
import cx_Oracle
|
||||
conn = cx_Oracle.connect(
|
||||
user=config['username'],
|
||||
password=config['password'],
|
||||
dsn=dsn
|
||||
)
|
||||
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Entferne Semikolon am Ende (Oracle mag das nicht bei einzelnen Statements)
|
||||
cleaned_query = query.strip()
|
||||
if cleaned_query.endswith(';'):
|
||||
cleaned_query = cleaned_query[:-1]
|
||||
|
||||
cursor.execute(cleaned_query)
|
||||
|
||||
# Wenn es eine SELECT-Query ist, hole Ergebnisse
|
||||
if query.strip().lower().startswith('select'):
|
||||
columns = [desc[0] for desc in cursor.description]
|
||||
data = cursor.fetchall()
|
||||
|
||||
# Konvertiere Oracle-spezifische Datentypen
|
||||
processed_data = []
|
||||
for row in data:
|
||||
processed_row = []
|
||||
for value in row:
|
||||
# Konvertiere LOB, CLOB, BLOB zu String
|
||||
if hasattr(value, 'read'):
|
||||
processed_row.append(value.read())
|
||||
# Konvertiere Oracle NUMBER zu Python-Typen
|
||||
elif str(type(value)).find('NUMBER') != -1:
|
||||
processed_row.append(float(value) if '.' in str(value) else int(value))
|
||||
else:
|
||||
processed_row.append(value)
|
||||
processed_data.append(processed_row)
|
||||
|
||||
return {
|
||||
'columns': columns,
|
||||
'data': processed_data,
|
||||
'row_count': len(processed_data)
|
||||
}
|
||||
else:
|
||||
# Für INSERT, UPDATE, DELETE etc.
|
||||
conn.commit()
|
||||
return {
|
||||
'message': f'Query erfolgreich ausgeführt. {cursor.rowcount} Zeilen betroffen.',
|
||||
'affected_rows': cursor.rowcount
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
raise e
|
||||
finally:
|
||||
cursor.close()
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"Oracle-Datenbankfehler: {str(e)}")
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
def get_tables(self):
|
||||
"""Hole alle Oracle Tabellennamen"""
|
||||
return self._get_oracle_tables()
|
||||
|
||||
def _get_oracle_tables(self):
|
||||
"""Hole Oracle Tabellennamen"""
|
||||
if not ORACLE_AVAILABLE:
|
||||
raise Exception("Oracle-Treiber nicht verfügbar")
|
||||
|
||||
config = self.connection_config
|
||||
dsn = f"{config['host']}:{config.get('port', 1521)}/{config.get('service_name', 'ORCL')}"
|
||||
|
||||
try:
|
||||
try:
|
||||
import oracledb
|
||||
conn = oracledb.connect(
|
||||
user=config['username'],
|
||||
password=config['password'],
|
||||
dsn=dsn
|
||||
)
|
||||
except ImportError:
|
||||
import cx_Oracle
|
||||
conn = cx_Oracle.connect(
|
||||
user=config['username'],
|
||||
password=config['password'],
|
||||
dsn=dsn
|
||||
)
|
||||
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Hole alle Tabellen für den aktuellen Benutzer
|
||||
cursor.execute("""
|
||||
SELECT table_name
|
||||
FROM user_tables
|
||||
ORDER BY table_name
|
||||
""")
|
||||
|
||||
tables = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return tables
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"Fehler beim Laden der Oracle-Tabellen: {str(e)}")
|
||||
|
||||
def get_table_schema(self, table_name):
|
||||
"""Hole Oracle Tabellenschema"""
|
||||
return self._get_oracle_table_schema(table_name)
|
||||
|
||||
def _get_oracle_table_schema(self, table_name):
|
||||
"""Hole Oracle Tabellenschema"""
|
||||
if not ORACLE_AVAILABLE:
|
||||
raise Exception("Oracle-Treiber nicht verfügbar")
|
||||
|
||||
config = self.connection_config
|
||||
dsn = f"{config['host']}:{config.get('port', 1521)}/{config.get('service_name', 'ORCL')}"
|
||||
|
||||
try:
|
||||
try:
|
||||
import oracledb
|
||||
conn = oracledb.connect(
|
||||
user=config['username'],
|
||||
password=config['password'],
|
||||
dsn=dsn
|
||||
)
|
||||
except ImportError:
|
||||
import cx_Oracle
|
||||
conn = cx_Oracle.connect(
|
||||
user=config['username'],
|
||||
password=config['password'],
|
||||
dsn=dsn
|
||||
)
|
||||
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Hole Spalten-Informationen
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
c.column_name,
|
||||
c.data_type,
|
||||
c.data_length,
|
||||
c.data_precision,
|
||||
c.data_scale,
|
||||
c.nullable,
|
||||
c.data_default,
|
||||
CASE WHEN p.column_name IS NOT NULL THEN 'Y' ELSE 'N' END as primary_key
|
||||
FROM user_tab_columns c
|
||||
LEFT JOIN (
|
||||
SELECT acc.column_name, acc.table_name
|
||||
FROM user_constraints ac, user_cons_columns acc
|
||||
WHERE ac.constraint_name = acc.constraint_name
|
||||
AND ac.constraint_type = 'P'
|
||||
AND ac.table_name = :table_name
|
||||
) p ON c.column_name = p.column_name AND c.table_name = p.table_name
|
||||
WHERE c.table_name = :table_name
|
||||
ORDER BY c.column_id
|
||||
""", {'table_name': table_name.upper()})
|
||||
|
||||
columns = []
|
||||
for row in cursor.fetchall():
|
||||
data_type = row[1]
|
||||
if row[2]: # data_length
|
||||
if row[3]: # data_precision
|
||||
data_type += f"({row[3]}"
|
||||
if row[4]: # data_scale
|
||||
data_type += f",{row[4]}"
|
||||
data_type += ")"
|
||||
elif row[1] in ('VARCHAR2', 'CHAR', 'NVARCHAR2', 'NCHAR'):
|
||||
data_type += f"({row[2]})"
|
||||
|
||||
columns.append({
|
||||
'name': row[0],
|
||||
'type': data_type,
|
||||
'not_null': row[5] == 'N',
|
||||
'default_value': row[6],
|
||||
'primary_key': row[7] == 'Y'
|
||||
})
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return columns
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"Fehler beim Laden des Oracle-Tabellenschemas: {str(e)}")
|
||||
@@ -0,0 +1,753 @@
|
||||
/* Hauptstyles für die Query Builder Anwendung */
|
||||
|
||||
/* Basis-Reset und Layout */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f8f9fa;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Fallback für Bootstrap Klassen falls CDN nicht lädt */
|
||||
.container-fluid {
|
||||
width: 100%;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-right: -15px;
|
||||
margin-left: -15px;
|
||||
}
|
||||
|
||||
.col-md-3 {
|
||||
flex: 0 0 25%;
|
||||
max-width: 25%;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.col-md-9 {
|
||||
flex: 0 0 75%;
|
||||
max-width: 75%;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.h-100 {
|
||||
height: 100vh !important;
|
||||
}
|
||||
|
||||
/* Navigation Bar */
|
||||
.navbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: #343a40 !important;
|
||||
color: white;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
padding-top: 0.3125rem;
|
||||
padding-bottom: 0.3125rem;
|
||||
margin-right: 1rem;
|
||||
font-size: 1.25rem;
|
||||
color: white !important;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.navbar-text {
|
||||
color: rgba(255,255,255,.5) !important;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: rgba(255,255,255,.5) !important;
|
||||
text-decoration: none;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: rgba(255,255,255,.75) !important;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-block;
|
||||
font-weight: 400;
|
||||
color: #212529;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
border-radius: 0.25rem;
|
||||
text-decoration: none;
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #007bff;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
color: #fff;
|
||||
background-color: #0056b3;
|
||||
border-color: #004085;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
color: #fff;
|
||||
background-color: #28a745;
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
color: #fff;
|
||||
background-color: #1e7e34;
|
||||
border-color: #1c7430;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
color: #fff;
|
||||
background-color: #6c757d;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
color: #fff;
|
||||
background-color: #545b62;
|
||||
border-color: #4e555b;
|
||||
}
|
||||
|
||||
.btn-outline-primary {
|
||||
color: #007bff;
|
||||
border-color: #007bff;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.btn-outline-primary:hover {
|
||||
color: #fff;
|
||||
background-color: #007bff;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.btn-outline-success {
|
||||
color: #28a745;
|
||||
border-color: #28a745;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.btn-outline-success:hover {
|
||||
color: #fff;
|
||||
background-color: #28a745;
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.btn-outline-danger {
|
||||
color: #dc3545;
|
||||
border-color: #dc3545;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.btn-outline-danger:hover {
|
||||
color: #fff;
|
||||
background-color: #dc3545;
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.15rem 0.4rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 0.15rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.btn-xs {
|
||||
padding: 0.1rem 0.25rem;
|
||||
font-size: 0.65rem;
|
||||
border-radius: 0.1rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.btn-group > .btn {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
margin-right: -1px;
|
||||
}
|
||||
|
||||
.btn-group > .btn:first-child {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.btn-group > .btn:not(:first-child):not(:last-child) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.btn-group > .btn:last-child {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
word-wrap: break-word;
|
||||
background-color: #fff;
|
||||
background-clip: border-box;
|
||||
border: 1px solid rgba(0,0,0,.125);
|
||||
border-radius: 0.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
flex: 1 1 auto;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 0.75rem 1.25rem;
|
||||
margin-bottom: 0;
|
||||
background-color: rgba(0,0,0,.03);
|
||||
border-bottom: 1px solid rgba(0,0,0,.125);
|
||||
border-top-left-radius: calc(0.25rem - 1px);
|
||||
border-top-right-radius: calc(0.25rem - 1px);
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 0.75rem 1.25rem;
|
||||
background-color: rgba(0,0,0,.03);
|
||||
border-top: 1px solid rgba(0,0,0,.125);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.form-control {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #495057;
|
||||
background-color: #fff;
|
||||
background-clip: padding-box;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 0.25rem;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
color: #495057;
|
||||
background-color: #fff;
|
||||
border-color: #80bdff;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.alert {
|
||||
position: relative;
|
||||
padding: 0.75rem 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
color: #0c5460;
|
||||
background-color: #d1ecf1;
|
||||
border-color: #bee5eb;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
color: #155724;
|
||||
background-color: #d4edda;
|
||||
border-color: #c3e6cb;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
color: #721c24;
|
||||
background-color: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
}
|
||||
|
||||
.alert-dismissible {
|
||||
padding-right: 4rem;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.table {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
color: #212529;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 0.75rem;
|
||||
vertical-align: top;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
vertical-align: bottom;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
background-color: #f8f9fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.table-striped tbody tr:nth-of-type(odd) {
|
||||
background-color: rgba(0,0,0,.05);
|
||||
}
|
||||
|
||||
.table-hover tbody tr:hover {
|
||||
color: #212529;
|
||||
background-color: rgba(0,0,0,.075);
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.text-center { text-align: center !important; }
|
||||
.text-muted { color: #6c757d !important; }
|
||||
.text-danger { color: #dc3545 !important; }
|
||||
.text-success { color: #28a745 !important; }
|
||||
.text-primary { color: #007bff !important; }
|
||||
.text-info { color: #17a2b8 !important; }
|
||||
.text-warning { color: #ffc107 !important; }
|
||||
|
||||
.bg-light { background-color: #f8f9fa !important; }
|
||||
.bg-dark { background-color: #343a40 !important; }
|
||||
|
||||
.border-end { border-right: 1px solid #dee2e6 !important; }
|
||||
|
||||
.mb-0 { margin-bottom: 0 !important; }
|
||||
.mb-1 { margin-bottom: 0.25rem !important; }
|
||||
.mb-2 { margin-bottom: 0.5rem !important; }
|
||||
.mb-3 { margin-bottom: 1rem !important; }
|
||||
.mb-4 { margin-bottom: 1.5rem !important; }
|
||||
|
||||
.mt-2 { margin-top: 0.5rem !important; }
|
||||
.mt-3 { margin-top: 1rem !important; }
|
||||
.mt-5 { margin-top: 3rem !important; }
|
||||
|
||||
.p-2 { padding: 0.5rem !important; }
|
||||
.p-3 { padding: 1rem !important; }
|
||||
.py-4 { padding-top: 1.5rem !important; padding-bottom: 1.5rem !important; }
|
||||
|
||||
.d-flex { display: flex !important; }
|
||||
.d-grid { display: grid !important; }
|
||||
.justify-content-between { justify-content: space-between !important; }
|
||||
.justify-content-center { justify-content: center !important; }
|
||||
.align-items-center { align-items: center !important; }
|
||||
|
||||
.float-end { float: right !important; }
|
||||
.ms-auto { margin-left: auto !important; }
|
||||
.me-3 { margin-right: 1rem !important; }
|
||||
|
||||
/* Sidebar Styles */
|
||||
.col-md-3.border-end {
|
||||
height: calc(100vh - 56px);
|
||||
overflow-y: auto;
|
||||
background-color: #f8f9fa;
|
||||
border-right: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.col-md-9 {
|
||||
height: calc(100vh - 56px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Tabellen und Query Cards */
|
||||
.saved-query-card {
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.saved-query-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.table-container {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.table-item {
|
||||
padding: 4px 8px;
|
||||
margin: 1px 0;
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.table-item:hover {
|
||||
background: #f8f9fa;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.table-item.selected {
|
||||
background: #e3f2fd;
|
||||
border-color: #2196f3;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.table-item i {
|
||||
color: #6c757d;
|
||||
margin-right: 6px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Spalten-Anzeige unter Tabellen */
|
||||
.table-columns {
|
||||
margin-left: 8px;
|
||||
margin-top: 2px;
|
||||
padding: 4px 6px;
|
||||
background: #f8f9fa;
|
||||
border-left: 2px solid #007bff;
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
|
||||
.column-item {
|
||||
padding: 2px 6px;
|
||||
margin: 0.5px 0;
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.column-item:hover {
|
||||
background: #e7f3ff;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.column-name {
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.column-type {
|
||||
font-size: 9px;
|
||||
color: #6c757d;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Toggle-Button für Spalten-Anzeige */
|
||||
#toggle-columns-btn {
|
||||
margin-right: 3px;
|
||||
transition: all 0.3s ease;
|
||||
padding: 0.1rem 0.3rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
#toggle-columns-btn:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
#toggle-columns-btn.btn-outline-secondary {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
#toggle-columns-btn.btn-outline-primary {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Drag & Drop Styles */
|
||||
.table-item, .column-item {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.table-item:active, .column-item:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.drag-icon {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
margin-left: auto;
|
||||
color: #6c757d;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.table-item:hover .drag-icon,
|
||||
.column-item:hover .drag-icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Drop Zone Styles */
|
||||
#query-input.drag-over {
|
||||
border-color: #007bff !important;
|
||||
box-shadow: 0 0 10px rgba(0, 123, 255, 0.3) !important;
|
||||
background-color: #f8f9ff !important;
|
||||
}
|
||||
|
||||
.table-item[draggable="true"]:hover,
|
||||
.column-item[draggable="true"]:hover {
|
||||
background-color: #e3f2fd;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* Query Input Area */
|
||||
#query-input {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
resize: vertical;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
/* Results Table */
|
||||
.results-table {
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.results-table table {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.results-table th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #007bff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Alert Styles */
|
||||
.alert-custom {
|
||||
margin: 0 0 15px 0;
|
||||
padding: 10px 15px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.alert-success-custom {
|
||||
background-color: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.alert-error-custom {
|
||||
background-color: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.col-md-3.border-end {
|
||||
height: auto;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.col-md-9 {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
height: auto;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.row.h-100 {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Button Improvements */
|
||||
.btn-group-sm > .btn, .btn-sm {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Card Improvements */
|
||||
.card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
/* Navigation Improvements */
|
||||
.navbar-brand {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Modal Improvements */
|
||||
.modal-content {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
/* Table Schema Popup */
|
||||
.schema-popup {
|
||||
position: absolute;
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
z-index: 1000;
|
||||
max-width: 300px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Autocomplete Dropdown */
|
||||
.autocomplete-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 1px solid #007bff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
z-index: 1050;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.autocomplete-item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: background-color 0.2s ease;
|
||||
font-size: 0.9rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.autocomplete-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.autocomplete-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.autocomplete-item.selected {
|
||||
background-color: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.autocomplete-item i {
|
||||
margin-right: 8px;
|
||||
color: #6c757d;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.autocomplete-item small {
|
||||
margin-left: auto;
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.autocomplete-item strong {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.autocomplete-header {
|
||||
padding: 6px 12px;
|
||||
background-color: #f8f9fa;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.schema-column {
|
||||
padding: 2px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.schema-column:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
+9
File diff suppressed because one or more lines are too long
Vendored
+9
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
# Placeholder für favicon - in Produktion sollte hier eine echte .ico Datei sein
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,47 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Login - Query Builder{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center mt-5">
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="text-center mb-0">
|
||||
<i class="fas fa-sign-in-alt"></i> Anmelden
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Benutzername</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Passwort</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-sign-in-alt"></i> Anmelden
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
<a href="{{ url_for('auth.register') }}" class="text-decoration-none">
|
||||
Noch kein Konto? Hier registrieren
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 p-3 bg-light rounded">
|
||||
<h6>Demo-Zugang:</h6>
|
||||
<p class="small mb-1"><strong>Benutzername:</strong> admin</p>
|
||||
<p class="small mb-0"><strong>Passwort:</strong> admin123</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,46 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Registrierung - Query Builder{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center mt-5">
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="text-center mb-0">
|
||||
<i class="fas fa-user-plus"></i> Registrierung
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Benutzername</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">E-Mail</label>
|
||||
<input type="email" class="form-control" id="email" name="email" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Passwort</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-user-plus"></i> Registrieren
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
<a href="{{ url_for('auth.login') }}" class="text-decoration-none">
|
||||
Bereits ein Konto? Hier anmelden
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,61 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Query Builder{% endblock %}</title>
|
||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
<!-- Bootstrap CSS (lokal gehostet) -->
|
||||
<link href="{{ url_for('static', filename='css/vendor/bootstrap.min.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- Font Awesome Icons (lokal gehostet) -->
|
||||
<link href="{{ url_for('static', filename='css/vendor/all-local.min.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- Custom Styles (Fallback für Bootstrap + eigene Styles) -->
|
||||
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ url_for('main.dashboard') }}">
|
||||
<i class="fas fa-database"></i> Query Builder
|
||||
</a>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="navbar-nav ms-auto">
|
||||
<span class="navbar-text me-3">
|
||||
<i class="fas fa-user"></i> {{ current_user.username }}
|
||||
</span>
|
||||
<a class="nav-link" href="{{ url_for('auth.logout') }}">
|
||||
<i class="fas fa-sign-out-alt"></i> Abmelden
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container-fluid">
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-info alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- Bootstrap JS (lokal gehostet) -->
|
||||
<script src="{{ url_for('static', filename='js/vendor/bootstrap.bundle.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,255 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard - Query Builder{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row h-100">
|
||||
<!-- Linke Sidebar - Tabellen -->
|
||||
<div class="col-md-2 border-end bg-light">
|
||||
<div class="p-3">
|
||||
<!-- Datenbankauswahl Section -->
|
||||
<div class="mb-4">
|
||||
<h6><i class="fas fa-database"></i> Datenbankverbindung</h6>
|
||||
<select id="database-selector" class="form-select form-select-sm" onchange="onDatabaseChange()">
|
||||
<option value="demo">Lade Verbindungen...</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-outline-info mt-2 w-100" onclick="testConnection()">
|
||||
<i class="fas fa-plug"></i> Verbindung testen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tabellen Section -->
|
||||
<div class="mb-4">
|
||||
<h5>
|
||||
<i class="fas fa-table"></i> Tabellen
|
||||
<div class="float-end">
|
||||
<button class="btn btn-sm btn-outline-secondary" id="toggle-columns-btn" onclick="toggleColumnsView()" title="Spalten ein/ausblenden">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="loadTables()">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</h5>
|
||||
<div id="tables-list">
|
||||
<div class="text-muted">Lade Tabellen...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hauptbereich - Query Editor und Ergebnisse -->
|
||||
<!-- Mittlere Spalte - Query Editor und Ergebnisse -->
|
||||
<div class="col-md-8">
|
||||
<div class="p-3">
|
||||
<!-- Query Editor Section -->
|
||||
<div class="mb-4">
|
||||
<h5><i class="fas fa-code"></i> SQL Query Editor</h5>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="position-relative">
|
||||
<textarea id="query-input" class="form-control mb-3" rows="8"
|
||||
placeholder="Geben Sie hier Ihre Oracle SQL-Query ein... Beispiel für Oracle: SELECT * FROM USERS WHERE ROWNUM <= 10"></textarea>
|
||||
<div id="autocomplete-dropdown" class="autocomplete-dropdown"></div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" onclick="executeQuery()">
|
||||
<i class="fas fa-play"></i> Query Ausführen
|
||||
</button>
|
||||
<button type="button" class="btn btn-success" id="save-query-btn">
|
||||
<i class="fas fa-save"></i> Query Speichern
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="clearQuery()">
|
||||
<i class="fas fa-trash"></i> Löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ergebnisse Section -->
|
||||
<div class="mb-3">
|
||||
<h5><i class="fas fa-table"></i> Query-Ergebnisse</h5>
|
||||
<div id="results-container" class="card">
|
||||
<div class="card-body">
|
||||
<div class="text-muted text-center py-4">
|
||||
Führen Sie eine Query aus, um Ergebnisse anzuzeigen
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rechte Sidebar - Gespeicherte Queries -->
|
||||
<div class="col-md-2 border-start bg-light">
|
||||
<div class="p-2 h-100">
|
||||
<div class="mb-3">
|
||||
<h6><i class="fas fa-bookmark"></i> Gespeicherte Queries</h6>
|
||||
</div>
|
||||
|
||||
<!-- Query-Suche -->
|
||||
<div class="mb-3">
|
||||
<input type="text" id="query-search" class="form-control form-control-sm"
|
||||
placeholder="Query suchen..." onkeyup="filterSavedQueries()">
|
||||
</div>
|
||||
|
||||
<!-- Gespeicherte Queries Liste -->
|
||||
<div id="saved-queries-list" class="overflow-auto" style="max-height: calc(100vh - 250px);">
|
||||
{% for query in saved_queries %}
|
||||
<div class="card mb-2 saved-query-card" data-query-id="{{ query.id }}">
|
||||
<div class="card-body p-2">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="flex-grow-1" style="cursor: pointer;" onclick="loadSavedQuery({{ query.id }})">
|
||||
<h6 class="card-title mb-1 text-primary">{{ query.name }}</h6>
|
||||
<small class="text-muted d-block mb-1">{{ query.created_at.strftime('%d.%m.%Y %H:%M') }}</small>
|
||||
<small class="text-muted query-preview">{{ query.query[:100] }}{% if query.query|length > 100 %}...{% endif %}</small>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#" onclick="loadSavedQuery({{ query.id }})">
|
||||
<i class="fas fa-play"></i> Laden
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="editQueryName({{ query.id }})">
|
||||
<i class="fas fa-edit"></i> Umbenennen
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="duplicateQuery({{ query.id }})">
|
||||
<i class="fas fa-copy"></i> Duplizieren
|
||||
</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item text-danger" href="#" onclick="deleteSavedQuery({{ query.id }})">
|
||||
<i class="fas fa-trash"></i> Löschen
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Platzhalter wenn keine Queries vorhanden -->
|
||||
<div id="no-queries-placeholder" class="text-center text-muted py-4" {% if saved_queries %}style="display: none;"{% endif %}>
|
||||
<i class="fas fa-bookmark fa-2x mb-2"></i>
|
||||
<p>Keine gespeicherten Queries vorhanden.</p>
|
||||
<small>Speichern Sie Ihre erste Query mit dem <i class="fas fa-save"></i> Button!</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Query Modal -->
|
||||
<div class="modal fade" id="saveQueryModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Query speichern</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="query-name" class="form-label">Query-Name:</label>
|
||||
<input type="text" class="form-control" id="query-name"
|
||||
placeholder="z.B. Benutzer-Liste" required autofocus>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="button" class="btn btn-success" onclick="saveQuery()">
|
||||
<i class="fas fa-save"></i> Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Query Name Modal -->
|
||||
<div class="modal fade" id="editQueryModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Query umbenennen</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="edit-query-name" class="form-label">Neuer Name:</label>
|
||||
<input type="text" class="form-control" id="edit-query-name" required autofocus>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="button" class="btn btn-primary" onclick="updateQueryName()">
|
||||
<i class="fas fa-save"></i> Umbenennen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Verbindungen werden automatisch in app.js geladen
|
||||
// currentConnection ist jetzt in app.js definiert
|
||||
|
||||
// Die loadConnections und onDatabaseChange Funktionen sind jetzt in app.js
|
||||
|
||||
// Teste Datenbankverbindung
|
||||
function testConnection() {
|
||||
const button = event.target;
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Teste...';
|
||||
button.disabled = true;
|
||||
|
||||
fetch(`/test_connection/${currentConnection}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showAlert('success', `✓ ${data.message}`);
|
||||
} else {
|
||||
showAlert('danger', `✗ ${data.message}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showAlert('danger', `Fehler beim Testen der Verbindung: ${error}`);
|
||||
})
|
||||
.finally(() => {
|
||||
button.innerHTML = originalText;
|
||||
button.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Hilfsfunktion für Alerts
|
||||
function showAlert(type, message) {
|
||||
// Entferne vorhandene Alerts
|
||||
const existingAlert = document.querySelector('.alert');
|
||||
if (existingAlert) {
|
||||
existingAlert.remove();
|
||||
}
|
||||
|
||||
// Erstelle neues Alert
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
|
||||
alertDiv.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
// Füge Alert am Anfang des Contents ein
|
||||
const content = document.querySelector('.container-fluid');
|
||||
content.insertBefore(alertDiv, content.firstChild);
|
||||
|
||||
// Auto-hide nach 5 Sekunden
|
||||
setTimeout(() => {
|
||||
if (alertDiv && alertDiv.parentNode) {
|
||||
alertDiv.remove();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user