diff --git a/my_flask_app/__pycache__/main.cpython-39.pyc b/my_flask_app/__pycache__/main.cpython-39.pyc index 7cff0738314b51c610d7db7ec14f520833d49a47..dd62806cbd3120a152b6117f29698eb8b6893a44 100644 Binary files a/my_flask_app/__pycache__/main.cpython-39.pyc and b/my_flask_app/__pycache__/main.cpython-39.pyc differ diff --git a/my_flask_app/main.py b/my_flask_app/main.py index 88190b45779015a0130f251cfd635ea3758a2cb1..5495326f0e021c74fe14ed40380bc657c9fc9cb5 100644 --- a/my_flask_app/main.py +++ b/my_flask_app/main.py @@ -1,10 +1,12 @@ from my_flask_app import app -from .models.models import CustomTable, CustomColumn, Theme +from .models.models import CustomTable, CustomColumn, Theme, CompressedDataType, RegType, RegRole from flask_sqlalchemy import SQLAlchemy -from flask import jsonify, redirect, render_template, request, url_for, session -from sqlalchemy import Inspector, MetaData, create_engine, text, inspect -import pydot, base64, os +from flask import jsonify, redirect, render_template, request, session, url_for +from sqlalchemy import ARRAY, BIGINT, BOOLEAN, DOUBLE_PRECISION, FLOAT, INTEGER, JSON, NUMERIC, SMALLINT, TIMESTAMP, UUID, VARCHAR, MetaData, String, create_engine, text, inspect +import pydot, base64, os, logging from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy.dialects.postgresql.base import ischema_names +from sqlalchemy.dialects.postgresql import JSONB, TSTZRANGE, INTERVAL, BYTEA # Set up database (call db.engine) @@ -14,54 +16,78 @@ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy() # db.init_app(app) + app.secret_key = 'my_secret_key' # Needed for session management dropped_items = [] +# Register the custom type with SQLAlchemy's PostgreSQL dialect +ischema_names['_timescaledb_internal.compressed_data'] = CompressedDataType +ischema_names['regtype'] = RegType +ischema_names['regrole'] = RegRole + +@app.errorhandler(400) +def log_bad_request(error): + logging.error(f"Bad request: {request} {request.data} {request.args}") + # You can add more request details to log here + return "Bad request", 400 + @app.route('/', methods=['POST', 'GET']) def index(): - if request.method == 'POST': - # Handle the form submission - database_uri = request.form['database_uri'] - session['db_uri'] = database_uri - engine = create_engine(database_uri) - session_factory = sessionmaker(bind=engine) - db.session = scoped_session(session_factory) - insp = inspect(engine) - metadata_obj = MetaData() - - # Determine the type and naem of database - database_type = engine.dialect.name # postgresql, sqlite - database = database_name_from_uri(engine, database_uri) - # Initialize variables - tables_selected = [] - schemas = getSchema(insp) - themes = getThemes() - - schema_Selected = request.form.get('schema', None) - show_all = request.form.get('show_all') == 'True' - - tables1 = importMetadata(engine, schema_Selected, tables_selected, show_all) - # graph_DOT1 = createGraph(tables1, themes["Blue Navy"], True, True, True) - # image1 = generate_erd(graph_DOT1) - - if dropped_items==[]: - image2 = "" - # else: - # tables2 = importMetadata(engine, None, dropped_items, False) - # graph_DOT2 = createGraph(tables2, themes["Blue Navy"], True, True, True) - # image2 = generate_erd(graph_DOT2) - - # return render_template('app.html', database=database, schemas=schemas, show_all=show_all, schema_Selected=schema_Selected, tables=tables1, image1=image1, image2=image2, dropped_items=dropped_items) + try: + print("1") + if request.method == 'POST': + print("2") + database_uri = request.form.get('database_uri') + print(database_uri) + if database_uri != '' and database_uri != None: + print("3") + if database_uri != session.get('db_uri', ''): + session['db_uri'] = database_uri + print("4") + # Initialize inspector here, outside the inner if-else + print("4.5") + engine = create_engine(database_uri) + insp = inspect(engine) + session_factory = sessionmaker(bind=engine) + db.session = scoped_session(session_factory) + metadata_obj = MetaData() + print("5") + + else: + database_uri = session.get('db_uri', '') + engine = create_engine(database_uri) + insp = inspect(engine) + session_factory = sessionmaker(bind=engine) + db.session = scoped_session(session_factory) + print("6") + print("6.5") + + database = database_name_from_uri(engine, database_uri) if database_uri != '' else '' + schemas = getSchema(insp) if session['db_uri'] != '' else [] + themes = getThemes() + tables_selected = [] + schema_Selected = request.form.get('schema', None) + show_all = request.form.get('show_all') == 'True' + + tables1 = importMetadata(engine, schema_Selected, tables_selected, show_all) + graph_DOT1 = createGraph(tables1, themes["Blue Navy"], True, True, True) + image1 = generate_erd(graph_DOT1) + + if dropped_items==[]: + image2 = "" + else: + tables2 = importMetadata(engine, None, dropped_items, False) + graph_DOT2 = createGraph(tables2, themes["Blue Navy"], True, True, True) + image2 = generate_erd(graph_DOT2) + + return render_template('app.html', database=database, schemas=schemas, show_all=show_all, schema_Selected=schema_Selected, tables=tables1, image1=image1, image2=image2, dropped_items=dropped_items) - - - # print(insp.get_foreign_keys('machine_sensor')) - # print(insp.get_unique_constraints('segmentation_data', schema='segmentation')) - - return render_template('app.html', database=database, schemas=schemas, show_all=show_all, schema_Selected=schema_Selected, tables=tables1, dropped_items=dropped_items) - else: - # Display the form - return render_template('app.html') + else: + # Display the form + return render_template('app.html') + + except Exception as e: + return f"An error occurred: {str(e)}", 400 @app.route('/handle-drop', methods=['POST']) @@ -107,7 +133,8 @@ def database_name_from_uri(engine, database_uri: str): return os.path.splitext(os.path.basename(database_uri.split('///')[-1]))[0] else: return 'Unknown' - + + def generate_html_table(content): if not content: return "No data found." @@ -192,14 +219,14 @@ def importMetadata(engine, schema=None, tables_selected=None, show_all=False): # If show_all is True, expand the list to include related tables. if show_all: - expand_to_include_related_tables(engine, tables) + tables = expand_to_include_related_tables(engine, tables) # Fetch columns for each table. - fetch_columns_for_tables(engine, tables) + tables = fetch_columns_for_tables(engine, tables) # Fetch constraints (PK, FK, Unique) for each table. - fetch_constraints_for_tables(engine, tables) - + tables = fetch_constraints_for_tables(engine, tables) + return tables @@ -222,6 +249,7 @@ def fetch_initial_tables(engine, schema=None, tables_selected_list=None): all_tables = insp.get_table_names() # Filter tables if a specific list is provided + table_names = [] if tables_selected_list: table_names = [table for table in all_tables if table in tables_selected_list] else: @@ -233,7 +261,7 @@ def fetch_initial_tables(engine, schema=None, tables_selected_list=None): table = CustomTable(table_name, table_schema) tables[table_name] = table table.label = f"n{len(tables)}" - + return tables @@ -242,6 +270,7 @@ def expand_to_include_related_tables(engine, tables): engine = create_engine(engine) # Create an inspector object insp = inspect(engine) + database_type = engine.dialect.name # This dictionary will temporarily store related tables to fetch later. related_tables_to_fetch = {} @@ -252,7 +281,8 @@ def expand_to_include_related_tables(engine, tables): fks = insp.get_foreign_keys(tableName, schema=table.schema) for fk in fks: referenced_table_name = fk['referred_table'] - referenced_schema = fk['referred_schema'] + # default schema is 'public' for postgresql + referenced_schema = insp.default_schema_name if fk['referred_schema']==None else fk['referred_schema'] if referenced_table_name not in tables and referenced_table_name not in related_tables_to_fetch: related_tables_to_fetch[referenced_table_name] = referenced_schema @@ -262,11 +292,12 @@ def expand_to_include_related_tables(engine, tables): # Create a CustomTable object for each related table. table = CustomTable(tableName, tableSchema) tables[tableName] = table + table.label = f"n{len(tables)}" return tables -def fetch_columns_for_tables(engine, tables): +def fetch_columns_for_tables(engine, tables): if isinstance(engine, str): engine = create_engine(engine) insp = inspect(engine) @@ -277,19 +308,61 @@ def fetch_columns_for_tables(engine, tables): for col in columns: name = col['name'] datatype = col['type'] + if isinstance(datatype, UUID): + datatype = "UUID" + elif isinstance(datatype, INTEGER): + datatype = "INTEGER" + elif isinstance(datatype, JSONB): + datatype = "JSONB" + elif isinstance(datatype, JSON): + datatype = "JSON" + elif isinstance(datatype, TIMESTAMP): + datatype = "TIMESTAMP" + elif isinstance(datatype, TSTZRANGE): + datatype = "TSTZRANGE" + elif isinstance(datatype, VARCHAR): + datatype = "VARCHAR" + elif isinstance(datatype, BOOLEAN): + datatype = "BOOLEAN" + elif isinstance(datatype, NUMERIC): + datatype = "NUMERIC" + elif isinstance(datatype, FLOAT): + datatype = "FLOAT" + elif isinstance(datatype, String): + datatype = "String" + elif isinstance(datatype, SMALLINT): + datatype = "SMALLINT" + elif isinstance(datatype, BIGINT): + datatype = "BIGINT" + elif isinstance(datatype, RegType): + datatype = "RegType" + elif isinstance(datatype, RegRole): + datatype = "RegRole" + elif isinstance(datatype, CompressedDataType): + datatype = "CompressedDataType" + elif isinstance(datatype, ARRAY): + datatype = "ARRAY" + elif isinstance(datatype, INTERVAL): + datatype = "INTERVAL" + elif isinstance(datatype, DOUBLE_PRECISION): + datatype = "DOUBLE_PRECISION" + elif isinstance(datatype, BYTEA): + datatype = "BYTEA" + + nullable = col['nullable'] default = col['default'] # Create a CustomColumn object with the retrieved information column = CustomColumn(table, name, '') column.setDataType({ - "type": str(datatype), + "type": datatype, "nullable": nullable, "default": default }) - # Append the column to the table's columns list table.columns.append(column) + return tables @@ -326,18 +399,343 @@ def fetch_constraints_for_tables(engine, tables): fk_columns = fk['constrained_columns'] referred_table = fk['referred_table'] referred_columns = fk['referred_columns'] - for fk_column, ref_column in zip(fk_columns, referred_columns): - column = table.getColumn(fk_column) - if column: - column.fkof = f"{referred_table}.{ref_column}" - if fk['name'] not in table.fks: - table.fks[fk['name']] = [] - table.fks[fk['name']].append(column) + + if referred_table in tables: + for (fk_column, ref_column) in zip(fk_columns, referred_columns): + fkColumn = table.getColumn(fk_column) + pkColumn = tables.get(referred_table).getColumn(ref_column) + + if fkColumn and pkColumn: + fkColumn.fkof = pkColumn + if fk['name'] not in table.fks: + table.fks[fk['name']] = [] + table.fks[fk['name']].append(fkColumn) return tables -# def fetch_constraints_for_tables(engine, tables): +def createGraph(tables, theme, showColumns, showTypes, useUpperCase): + s = ('digraph {\n' + + ' graph [ rankdir="LR" bgcolor="#ffffff" ]\n' + + f' node [ style="filled" shape="{theme.shape}" gradientangle="180" ]\n' + + ' edge [ arrowhead="none" arrowtail="none" dir="both" ]\n\n') + + for name in tables: + s += tables[name].getDotShape(theme, showColumns, showTypes, useUpperCase) + + s += "\n" + + for name in tables: + s += tables[name].getDotLinks(theme) + + s += "}\n" + + return s + + +def generate_erd(graph_DOT): + graph_module = pydot.graph_from_dot_data(graph_DOT) + graph = graph_module[0] + png_image_data = graph.create_png() + encoded_image = base64.b64encode(png_image_data).decode('utf-8') + return encoded_image + + +def getThemes(): + return { + "Common Gray": Theme("#6c6c6c", "#e0e0e0", "#f5f5f5", + "#e0e0e0", "#000000", "#000000", "rounded", "Mrecord", "#696969", "1"), + "Blue Navy": Theme("#1a5282", "#1a5282", "#ffffff", + "#1a5282", "#000000", "#ffffff", "rounded", "Mrecord", "#0078d7", "2"), + #"Gradient Green": Theme("#716f64", "#008080:#ffffff", "#008080:#ffffff", + # "transparent", "#000000", "#000000", "rounded", "Mrecord", "#696969", "1"), + #"Blue Sky": Theme("#716f64", "#d3dcef:#ffffff", "#d3dcef:#ffffff", + # "transparent", "#000000", "#000000", "rounded", "Mrecord", "#696969", "1"), + "Common Gray Box": Theme("#6c6c6c", "#e0e0e0", "#f5f5f5", + "#e0e0e0", "#000000", "#000000", "rounded", "record", "#696969", "1") + } + + +if __name__ == "__main__": + app.run(debug=True) + + + + + + + + +# from my_flask_app import app +# from flask_sqlalchemy import SQLAlchemy +# from flask import jsonify, render_template, request, session, send_file +# from sqlalchemy import text +# import re, pydot, base64 +# import pydot + + +# # Set up database +# app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +# app.config['SQLALCHEMY_DATABASE_URI'] = "postgresql://postgres:password@localhost:5432/test" + +# db = SQLAlchemy() +# db.init_app(app) + +# app.secret_key = 'my_secret_key' # Needed for session management +# dropped_items = [] + +# @app.route('/', methods=['POST', 'GET']) +# def index(): +# # Initialize variables +# database = db.engine.url.database +# tables_selected = [] +# # tables_selected = ['machine_sensor', 'machine_tool', 'machine_trace'] +# schemas = getSchema() +# themes = getThemes() + +# schema_Selected = request.form.get('schema', None) +# show_all = request.form.get('show_all') == 'True' + +# tables1 = importMetadata(database, schema_Selected, tables_selected, show_all) +# graph_DOT1 = createGraph(tables1, themes["Blue Navy"], True, True, True) +# image1 = generate_erd(graph_DOT1) + +# if dropped_items==[]: +# image2 = "" +# else: +# tables2 = importMetadata(database, None, dropped_items, False) +# graph_DOT2 = createGraph(tables2, themes["Blue Navy"], True, True, True) +# image2 = generate_erd(graph_DOT2) + +# print(getTableSchema('event_data')) + +# return render_template('app.html', database=database, schemas=schemas, show_all=show_all, schema_Selected=schema_Selected, tables=tables1, image1=image1, image2=image2, dropped_items=dropped_items) + + + +# @app.route('/handle-drop', methods=['POST']) +# def handle_drop(): +# data = request.json +# item_name = data.get('item') +# action = data.get('action') + +# if action == 'added': +# dropped_items.append(item_name) +# elif action == 'removed' and item_name in dropped_items: +# dropped_items.remove(item_name) + +# # Regenerate ERD based on the updated dropped_items +# database = db.engine.url.database +# themes = getThemes() +# tables2 = importMetadata(database, None, dropped_items, False) +# graph_DOT2 = createGraph(tables2, themes["Blue Navy"], True, True, True) +# image2 = generate_erd(graph_DOT2) + +# return jsonify(image2=image2) + + +# @app.route('/get-table-data', methods=['POST']) +# def get_table_data(): +# data = request.json +# table_name = data.get('table_name') +# print(table_name) + +# # Query your database to get the data for the table_name +# content = query_database_for_table_content(table_name) + +# # Convert content to HTML table format +# html_table = generate_html_table(content) + +# return jsonify({'html_table': html_table}) + + +# def generate_html_table(content): +# if not content: +# return "No data found." +# # Generate column headers +# columns = content[0] +# table_html = "<table class='uk-table uk-table-small uk-table-hover uk-table-divider'><thead><tr>" +# for col in columns: +# table_html += f"<th>{col}</th>" + +# table_html += "</tr></thead><tbody>" + +# # Generate table rows +# for i in range(1, len(content)): +# table_html += "<tr>" +# for item in content[i]: +# table_html += f"<td>{item}</td>" +# table_html += "</tr>" +# table_html += "</tbody></table>" +# return table_html + + +# def query_database_for_table_content(table_name, number=20): +# # Initialize content list +# content_list = [] + +# # Get the schema of the table +# schema = getTableSchema(table_name) +# # Query the database to get the content of the table +# sql_content = text(f"""SELECT * FROM {schema}.{table_name} LIMIT {number};""") +# result = db.session.execute(sql_content, {'table_name': table_name, 'number': number}).fetchall() + +# if not result: +# return [] + +# # Get the column names +# sql_columns = text(""" +# SELECT column_name +# FROM information_schema.columns +# WHERE table_name = :table_name; +# """) +# column_names = db.session.execute(sql_columns, {'table_name': table_name}).fetchall() + +# # Prepare column names +# columns = [column_name[0] for column_name in column_names] +# content_list.append(columns) + +# # Append rows to content list +# for row in result: +# content_list.append(list(row)) + +# return content_list + + +# def getTableSchema(table_name): +# sql= text(f""" +# SELECT table_schema +# FROM information_schema.tables +# WHERE table_name = :table_name; +# """) +# schema = db.session.execute(sql, {'table_name': table_name}).fetchone()[0] +# return schema + + +# def getSchema(): +# sql = text("""SELECT schema_name FROM information_schema.schemata;""") +# result = db.session.execute(sql) +# schemas = [row[0] for row in result] +# return schemas + + +# def importMetadata(database, schema=None, tables_selected=None, show_all=False): +# tables = {} +# if database == '': +# return tables + +# # Convert tables_selected to a list to ensure compatibility with SQL IN operation. +# tables_selected_list = list(tables_selected) if tables_selected else None + +# # Fetch initial tables based on schema and table_names. +# tables = fetch_initial_tables(schema, tables_selected_list) + +# # If show_all is True, expand the list to include related tables. +# if show_all: +# expand_to_include_related_tables(tables) + +# # Fetch columns for each table. +# fetch_columns_for_tables(tables) + +# # Fetch constraints (PK, FK, Unique) for each table. +# fetch_constraints_for_tables(tables) + +# return tables + + +# def fetch_initial_tables(schema, tables_selected_list): +# tables = {} + +# # Construct WHERE clauses based on input parameters. +# schema_condition = "AND table_schema = :schema" if schema else "" +# tables_selected_condition = "AND table_name = ANY(:tables_selected)" if tables_selected_list else "" + +# # Fetching tables with dynamic schema and table name filtering. +# sql_tables = text(f""" +# SELECT table_name, table_schema +# FROM information_schema.tables +# WHERE table_type = 'BASE TABLE' +# {schema_condition} {tables_selected_condition}; +# """) + +# # Adjust parameters based on the conditions. +# params = {} +# if schema: +# params['schema'] = schema +# if tables_selected_list: +# params['tables_selected'] = tables_selected_list + +# result = db.session.execute(sql_tables, params) + +# for row in result: +# tableName, tableSchema = row +# table = Table(tableName, tableSchema) # adding schema to the table comment +# tables[tableName] = table +# table.label = f"n{len(tables)}" + +# return tables + + +# def expand_to_include_related_tables(tables): +# # This dictionary will temporarily store related tables to fetch later. +# related_tables_to_fetch = {} + +# # Iterate over initially fetched tables to find foreign key relationships. +# for tableName, table in tables.items(): +# # Fetch foreign key relationships for the current table. +# sql_fk = text(""" +# SELECT +# ccu.table_name AS pk_table_name, +# ccu.table_schema AS pk_table_schema +# FROM +# information_schema.table_constraints AS tc +# JOIN information_schema.key_column_usage AS kcu +# ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema AND tc.table_name = kcu.table_name +# JOIN information_schema.constraint_column_usage AS ccu +# ON ccu.constraint_name = tc.constraint_name +# WHERE +# tc.constraint_type = 'FOREIGN KEY' +# AND tc.table_name = :table_name +# """) +# fk_result = db.session.execute(sql_fk, {'table_name': tableName}) + + +# for referenced_table_name, referenced_schema in fk_result: +# if referenced_table_name not in tables and referenced_table_name not in related_tables_to_fetch: +# related_tables_to_fetch[referenced_table_name] = referenced_schema + +# # Fetch and add related tables. +# for tableName, tableSchema in related_tables_to_fetch.items(): +# # Assuming a function fetch_table_details(tableName, tableSchema) that fetches and returns +# # a Table object with columns and constraints populated. +# table = Table(tableName, tableSchema) +# tables[tableName] = table + +# return tables + + +# def fetch_columns_for_tables(tables): +# for tableName, table in tables.items(): +# sql_columns = text(""" +# SELECT column_name, data_type, is_nullable, column_default +# FROM information_schema.columns +# WHERE table_name = :table_name; +# """) +# column_result = db.session.execute(sql_columns, {'table_name': tableName}) + +# for col in column_result: +# name, datatype, nullable, default = col +# column = Column(table, name, '') +# column.setDataType({ +# "type": datatype, +# "nullable": nullable == 'YES', +# "default": default +# }) +# table.columns.append(column) +# return tables + + +# def fetch_constraints_for_tables(tables): # # Fetching Unique Constraints # for tableName, table in tables.items(): # sql_unique = text(""" @@ -421,43 +819,178 @@ def fetch_constraints_for_tables(engine, tables): # return tables -def createGraph(tables, theme, showColumns, showTypes, useUpperCase): - s = ('digraph {\n' - + ' graph [ rankdir="LR" bgcolor="#ffffff" ]\n' - + f' node [ style="filled" shape="{theme.shape}" gradientangle="180" ]\n' - + ' edge [ arrowhead="none" arrowtail="none" dir="both" ]\n\n') - - for name in tables: - s += tables[name].getDotShape(theme, showColumns, showTypes, useUpperCase) - s += "\n" - for name in tables: - s += tables[name].getDotLinks(theme) - s += "}\n" - return s - - -def generate_erd(graph_DOT): - graph_module = pydot.graph_from_dot_data(graph_DOT) - graph = graph_module[0] - png_image_data = graph.create_png() - encoded_image = base64.b64encode(png_image_data).decode('utf-8') - return encoded_image - - -def getThemes(): - return { - "Common Gray": Theme("#6c6c6c", "#e0e0e0", "#f5f5f5", - "#e0e0e0", "#000000", "#000000", "rounded", "Mrecord", "#696969", "1"), - "Blue Navy": Theme("#1a5282", "#1a5282", "#ffffff", - "#1a5282", "#000000", "#ffffff", "rounded", "Mrecord", "#0078d7", "2"), - #"Gradient Green": Theme("#716f64", "#008080:#ffffff", "#008080:#ffffff", - # "transparent", "#000000", "#000000", "rounded", "Mrecord", "#696969", "1"), - #"Blue Sky": Theme("#716f64", "#d3dcef:#ffffff", "#d3dcef:#ffffff", - # "transparent", "#000000", "#000000", "rounded", "Mrecord", "#696969", "1"), - "Common Gray Box": Theme("#6c6c6c", "#e0e0e0", "#f5f5f5", - "#e0e0e0", "#000000", "#000000", "rounded", "record", "#696969", "1") - } - +# def createGraph(tables, theme, showColumns, showTypes, useUpperCase): +# s = ('digraph {\n' +# + ' graph [ rankdir="LR" bgcolor="#ffffff" ]\n' +# + f' node [ style="filled" shape="{theme.shape}" gradientangle="180" ]\n' +# + ' edge [ arrowhead="none" arrowtail="none" dir="both" ]\n\n') + +# for name in tables: +# s += tables[name].getDotShape(theme, showColumns, showTypes, useUpperCase) +# s += "\n" +# for name in tables: +# s += tables[name].getDotLinks(theme) +# s += "}\n" +# return s + + +# def generate_erd(graph_DOT): +# graph_module = pydot.graph_from_dot_data(graph_DOT) +# graph = graph_module[0] +# png_image_data = graph.create_png() +# encoded_image = base64.b64encode(png_image_data).decode('utf-8') +# return encoded_image + + +# def getThemes(): +# return { +# "Common Gray": Theme("#6c6c6c", "#e0e0e0", "#f5f5f5", +# "#e0e0e0", "#000000", "#000000", "rounded", "Mrecord", "#696969", "1"), +# "Blue Navy": Theme("#1a5282", "#1a5282", "#ffffff", +# "#1a5282", "#000000", "#ffffff", "rounded", "Mrecord", "#0078d7", "2"), +# #"Gradient Green": Theme("#716f64", "#008080:#ffffff", "#008080:#ffffff", +# # "transparent", "#000000", "#000000", "rounded", "Mrecord", "#696969", "1"), +# #"Blue Sky": Theme("#716f64", "#d3dcef:#ffffff", "#d3dcef:#ffffff", +# # "transparent", "#000000", "#000000", "rounded", "Mrecord", "#696969", "1"), +# "Common Gray Box": Theme("#6c6c6c", "#e0e0e0", "#f5f5f5", +# "#e0e0e0", "#000000", "#000000", "rounded", "record", "#696969", "1") +# } + + +# if __name__ == "__main__": +# app.run(port=5001) + + +# class Theme: +# def __init__(self, color, fillcolor, fillcolorC, +# bgcolor, icolor, tcolor, style, shape, pencolor, penwidth): +# self.color = color +# self.fillcolor = fillcolor +# self.fillcolorC = fillcolorC +# self.bgcolor = bgcolor +# self.icolor = icolor +# self.tcolor = tcolor +# self.style = style +# self.shape = shape +# self.pencolor = pencolor +# self.penwidth = penwidth + + +# class Table: +# def __init__(self, name, comment): +# self.name = name +# self.comment = comment if comment is not None and comment != 'None' else '' +# self.label = None + +# self.columns = [] # list of all columns +# self.uniques = {} # dictionary with UNIQUE constraints, by name + list of columns +# self.pks = [] # list of PK columns (if any) +# self.fks = {} # dictionary with FK constraints, by name + list of FK columns + + +# @classmethod +# def getClassName(cls, name, useUpperCase, withQuotes=True): +# if re.match("^[A-Z_0-9]*$", name) == None: +# return f'"{name}"' if withQuotes else name +# return name.upper() if useUpperCase else name.lower() + +# def getName(self, useUpperCase, withQuotes=True): +# return Table.getClassName(self.name, useUpperCase, withQuotes) + +# def getColumn(self, name): +# for column in self.columns: +# if column.name == name: +# return column +# return None + +# def getDotShape(self, theme, showColumns, showTypes, useUpperCase): +# fillcolor = theme.fillcolorC if showColumns else theme.fillcolor +# colspan = "2" if showTypes else "1" +# tableName = self.getName(useUpperCase, False) + +# s = (f' {self.label} [\n' +# + f' fillcolor="{fillcolor}" color="{theme.color}" penwidth="1"\n' +# + f' label=<<table style="{theme.style}" border="0" cellborder="0" cellspacing="0" cellpadding="1">\n' +# + f' <tr><td bgcolor="{theme.bgcolor}" align="center"' +# + f' colspan="{colspan}"><font color="{theme.tcolor}"><b>{tableName}</b></font></td></tr>\n') + +# if showColumns: +# for column in self.columns: +# name = column.getName(useUpperCase, False) +# if column.ispk: name = f"<u>{name}</u>" +# if column.fkof != None: name = f"<i>{name}</i>" +# if column.nullable: name = f"{name}*" +# if column.identity: name = f"{name} I" +# if column.isunique: name = f"{name} U" +# datatype = column.datatype +# if useUpperCase: datatype = datatype.upper() + +# if showTypes: +# s += (f' <tr><td align="left"><font color="{theme.icolor}">{name} </font></td>\n' +# + f' <td align="left"><font color="{theme.icolor}">{datatype}</font></td></tr>\n') +# else: +# s += f' <tr><td align="left"><font color="{theme.icolor}">{name}</font></td></tr>\n' + +# return s + ' </table>>\n ]\n' + + +# def getDotLinks(self, theme): +# s = "" +# for constraint in self.fks: +# fks = self.fks[constraint] +# fk1 = fks[0] +# dashed = "" if not fk1.nullable else ' style="dashed"' +# arrow = "" if fk1.ispk and len(self.pks) == len(fk1.fkof.table.pks) else ' arrowtail="crow"' +# s += (f' {self.label} -> {fk1.fkof.table.label}' +# + f' [ penwidth="{theme.penwidth}" color="{theme.pencolor}"{dashed}{arrow} ]\n') +# return s + -if __name__ == "__main__": - app.run(debug=True) \ No newline at end of file +# class Column: +# def __init__(self, table, name, comment): +# self.table = table +# self.name = name +# self.comment = comment if comment is not None and comment != 'None' else '' +# self.nullable = True +# self.datatype = None # with (length, or precision/scale) +# self.identity = False + +# self.isunique = False +# self.ispk = False +# self.pkconstraint = None +# self.fkof = None # points to the PK column on the other side + + +# def getName(self, useUpperCase, withQuotes=True): +# return Table.getClassName(self.name, useUpperCase, withQuotes) + + +# def setDataType(self, datatype): +# self.datatype = datatype["type"] +# self.nullable = bool(datatype["nullable"]) + +# if self.datatype == "FIXED": +# self.datatype = "NUMBER" +# elif "fixed" in datatype: +# fixed = bool(datatype["fixed"]) +# if self.datatype == "TEXT": +# self.datatype = "CHAR" if fixed else "VARCHAR" + +# if "length" in datatype: +# self.datatype += f"({str(datatype['length'])})" +# elif "scale" in datatype: +# if int(datatype['precision']) == 0: +# self.datatype += f"({str(datatype['scale'])})" +# if self.datatype == "TIMESTAMP_NTZ(9)": +# self.datatype = "TIMESTAMP" +# elif "scale" in datatype and int(datatype['scale']) == 0: +# self.datatype += f"({str(datatype['precision'])})" +# if self.datatype == "NUMBER(38)": +# self.datatype = "INT" +# elif self.datatype.startswith("NUMBER("): +# self.datatype = f"INT({str(datatype['precision'])})" +# elif "scale" in datatype: +# self.datatype += f"({str(datatype['precision'])},{str(datatype['scale'])})" +# #if column.datatype.startswith("NUMBER("): +# # column.datatype = f"FLOAT({str(datatype['precision'])},{str(datatype['scale'])})" +# self.datatype = self.datatype.lower() \ No newline at end of file diff --git a/my_flask_app/models/__pycache__/models.cpython-39.pyc b/my_flask_app/models/__pycache__/models.cpython-39.pyc index 922505e8e007a16202bcb47fb6b19cc560709207..b9d512653cb5762711304af41da98b06d94de3e5 100644 Binary files a/my_flask_app/models/__pycache__/models.cpython-39.pyc and b/my_flask_app/models/__pycache__/models.cpython-39.pyc differ diff --git a/my_flask_app/models/models.py b/my_flask_app/models/models.py index 118e0c5ec238724cc92f55392aa10e04695175aa..20e0597b5325d166663580d76161f812e8f37ee7 100644 --- a/my_flask_app/models/models.py +++ b/my_flask_app/models/models.py @@ -1,5 +1,20 @@ +from sqlalchemy.types import UserDefinedType import re + +class CompressedDataType(UserDefinedType): + def get_col_spec(self): + return "_timescaledb_internal.compressed_data" + +class RegType(UserDefinedType): + def get_col_spec(self): + return "regtype" + +class RegRole(UserDefinedType): + def get_col_spec(self): + return "regrole" + + class Theme: def __init__(self, color, fillcolor, fillcolorC, bgcolor, icolor, tcolor, style, shape, pencolor, penwidth): diff --git a/my_flask_app/templates/app.html b/my_flask_app/templates/app.html index 99e8ff95d484cfcfb1e36ea847ae3a659291db86..7895a8d0add68c77eb4f2336094d734c1d974d24 100644 --- a/my_flask_app/templates/app.html +++ b/my_flask_app/templates/app.html @@ -2,11 +2,12 @@ <html lang="en"> <head> <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Database ERD Viewer</title> <link rel="stylesheet" href="{{ url_for('static', filename='uikit-3.18.0/css/uikit.min.css') }}" /> <script src="{{ url_for('static', filename='uikit-3.18.0/js/uikit.min.js') }}"></script> <script src="{{ url_for('static', filename='uikit-3.18.0/js/uikit-icons.min.js') }}"></script> - + <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"> <script src="https://d3js.org/d3.v5.min.js"></script> <script src="https://unpkg.com/d3-graphviz@2.6.1/build/d3-graphviz.min.js"></script> <style> @@ -22,7 +23,7 @@ } #sidebar1 { /* position: relative; */ - width: 15%; + width: 20%; min-height: 100vh; border-right: 0px solid #ccc; background-color: #add8e6; @@ -77,6 +78,9 @@ /* padding-top: 0px; padding-bottom: 0px; */ } + .accordion-button { + height: 8px; + } .custom-push-button { font-size: 14px; width: 30px; @@ -214,21 +218,24 @@ </style> </head> <body> - <div id="sidebar1"> <!--class="uk-panel uk-panel-scrollable" --> + <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r" crossorigin="anonymous"></script> + <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js" integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy" crossorigin="anonymous"></script> + + <div id="sidebar1"> <!--class="uk-panel uk-panel-scrollable" --> <h4>Database:</h4> <p style="margin-top: -10px; padding-left: 5px;">{{database}}.db</p> - <ul uk-accordion> + <!-- <ul uk-accordion> <li class="uk-open"> <a class="uk-accordion-title" href="#" style="margin-top: -8px; margin-bottom: -1px;">Schema:</a> <div class="uk-accordion-content" style="margin-top: -3px; border: 1px solid black;"> <form method="post"> - <label> - <input class="uk-checkbox" style="margin: 2px 2px 2px 5px; border-color: black;" type="checkbox" name="show_all" value="True" onchange="this.form.submit()" {{ 'checked' if show_all else '' }}> - <span class="uk-label" style="margin: 1px;" >show all</span> + <input class="form-check-input" id="flexCheckDefault" style="margin: 2px 2px 2px 5px; border-color: black;" type="checkbox" name="show_all" value="True" onchange="this.form.submit()" {{ 'checked' if show_all else '' }}> + <label class="form-check-label" for="flexCheckDefault" style="margin-top: 3px;"> + Primary Tables </label> - <select class="uk-select" name="schema" onchange='this.form.submit()' style="padding: 0px 1px 2px 1px;"> + <select class="form-select" name="schema" onchange='this.form.submit()' style="padding: 0px 1px 2px 1px;"> <option value="">Choose here</option> {% for schema in schemas %} <option value="{{ schema }}" {% if schema == schema_Selected %} selected {% endif %}>{{ schema }}</option> @@ -246,7 +253,72 @@ <li> <a class="uk-accordion-title" href="#">Label:</a> </li> - </ul> + </ul> --> + + <div class="accordion" id="accordionExample"> + <div class="accordion-item"> + <h2 class="accordion-header"> + <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne"> + Schema + </button> + </h2> + <div id="collapseOne" class="accordion-collapse collapse show" data-bs-parent="#accordionExample"> + <div class="accordion-body"> + <form method="post"> + <!-- <label> --> + <input class="form-check-input" id="flexCheckDefault" style="margin: 2px 2px 2px 5px; border-color: black;" type="checkbox" name="show_all" value="True" onchange="this.form.submit()" {{ 'checked' if show_all else '' }}> + <!-- <span class="uk-label" style="margin: 1px;" >show all</span> --> + <label class="form-check-label" for="flexCheckDefault" style="margin-top: 3px;"> + Primary Tables + </label> + <!-- </label> --> + <select class="form-select" name="schema" onchange='this.form.submit()' style="padding: 0px 1px 2px 1px;"> + <option value="">Choose here</option> + {% for schema in schemas %} + <option value="{{ schema }}" {% if schema == schema_Selected %} selected {% endif %}>{{ schema }}</option> + {% endfor %} + </select> + </form> + </div> + </div> + </div> + <div class="accordion-item"> + <h2 class="accordion-header"> + <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTwo" aria-expanded="true" aria-controls="collapseTwo"> + Time + </button> + </h2> + <div id="collapseTwo" class="accordion-collapse collapse show" data-bs-parent="#accordionExample"> + <div class="accordion-body"> + <!-- <strong>This is the first item's accordion body.</strong> It is shown by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the <code>.accordion-body</code>, though the transition does limit overflow. --> + </div> + </div> + </div> + <div class="accordion-item"> + <h2 class="accordion-header"> + <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree"> + Object + </button> + </h2> + <div id="collapseThree" class="accordion-collapse collapse" data-bs-parent="#accordionExample"> + <div class="accordion-body"> + <!-- <strong>This is the second item's accordion body.</strong> It is hidden by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the <code>.accordion-body</code>, though the transition does limit overflow. --> + </div> + </div> + </div> + <div class="accordion-item"> + <h2 class="accordion-header"> + <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseFour" aria-expanded="false" aria-controls="collapseFour"> + Label + </button> + </h2> + <div id="collapseFour" class="accordion-collapse collapse" data-bs-parent="#accordionExample"> + <div class="accordion-body"> + <!-- <strong>This is the third item's accordion body.</strong> It is hidden by default, until the collapse plugin adds the appropriate classes that we use to style each element. These classes control the overall appearance, as well as the showing and hiding via CSS transitions. You can modify any of this with custom CSS or overriding our default variables. It's also worth noting that just about any HTML can go within the <code>.accordion-body</code>, though the transition does limit overflow. --> + </div> + </div> + </div> + </div> <h4 style="margin-top: 0px;">Tables:</h4> <div id="table_list" style="margin-bottom: 5px; margin-top: -10px;"> @@ -386,10 +458,15 @@ <!-- set_database.html --> <form method="post" action="/"> - <input type="text" name="database_uri" placeholder="Enter Database URI"> - <input type="submit" value="Set Database"> + <div class="mb-3"> + <label for="exampleInputPassword1" name="database_uri" class="form-label">Database URI</label> + <input type="text" class="form-control" id="exampleInputPassword1" name="database_uri" placeholder="Enter Database URI"> + <button type="submit" class="btn btn-primary">Submit</button> + </div> </form> + + <div id="resize-handle-left" class="resize-handle-left"></div> </div>