Add SQL insight dashboard widgets
This commit is contained in:
@@ -162,6 +162,7 @@ cython_debug/
|
|||||||
|
|
||||||
# Exclude backup sql files
|
# Exclude backup sql files
|
||||||
**/*.sql
|
**/*.sql
|
||||||
|
!migrations/2026-09-06_add_dashboard_widgets.sql
|
||||||
|
|
||||||
# Exclude experimental juypter notebooks
|
# Exclude experimental juypter notebooks
|
||||||
**/*.ipynb
|
**/*.ipynb
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ def dashboard():
|
|||||||
exercises = db.dashboard.get_exercises_with_selection(selected_people_ids, min_date, max_date, selected_exercise_ids)
|
exercises = db.dashboard.get_exercises_with_selection(selected_people_ids, min_date, max_date, selected_exercise_ids)
|
||||||
tags = db.get_tags_for_dashboard()
|
tags = db.get_tags_for_dashboard()
|
||||||
dashboard = db.dashboard.get(selected_people_ids, min_date, max_date, selected_exercise_ids)
|
dashboard = db.dashboard.get(selected_people_ids, min_date, max_date, selected_exercise_ids)
|
||||||
|
pinned_widgets = db.dashboard.get_pinned_widgets(current_user.id) if current_user.is_authenticated else []
|
||||||
|
|
||||||
# Render the appropriate response for HTMX or full page
|
# Render the appropriate response for HTMX or full page
|
||||||
render_args = {
|
render_args = {
|
||||||
@@ -119,6 +120,7 @@ def dashboard():
|
|||||||
"people": people,
|
"people": people,
|
||||||
"exercises": exercises,
|
"exercises": exercises,
|
||||||
"tags": tags,
|
"tags": tags,
|
||||||
|
"pinned_widgets": pinned_widgets,
|
||||||
"selected_people_ids": selected_people_ids,
|
"selected_people_ids": selected_people_ids,
|
||||||
"max_date": max_date,
|
"max_date": max_date,
|
||||||
"min_date": min_date,
|
"min_date": min_date,
|
||||||
|
|||||||
+48
-1
@@ -1,10 +1,57 @@
|
|||||||
from utils import calculate_estimated_1rm, get_exercise_graph_model
|
from utils import calculate_estimated_1rm, get_exercise_graph_model, prepare_svg_plot_data
|
||||||
|
|
||||||
|
|
||||||
class Dashboard:
|
class Dashboard:
|
||||||
def __init__(self, db_connection_method):
|
def __init__(self, db_connection_method):
|
||||||
self.execute = db_connection_method
|
self.execute = db_connection_method
|
||||||
|
|
||||||
|
def get_pinned_widgets(self, person_id):
|
||||||
|
"""Execute and prepare the current user's pinned SQL dashboard widgets."""
|
||||||
|
widgets = self.execute("""
|
||||||
|
SELECT
|
||||||
|
w.widget_id,
|
||||||
|
w.saved_query_id AS query_id,
|
||||||
|
w.visualization_type,
|
||||||
|
q.title,
|
||||||
|
q.query
|
||||||
|
FROM dashboard_widget w
|
||||||
|
INNER JOIN saved_query q ON q.id = w.saved_query_id
|
||||||
|
WHERE w.person_id = %s AND w.is_visible = TRUE
|
||||||
|
ORDER BY w.position, w.widget_id
|
||||||
|
""", [person_id]) or []
|
||||||
|
|
||||||
|
prepared_widgets = []
|
||||||
|
for widget in widgets:
|
||||||
|
prepared = dict(widget)
|
||||||
|
prepared['results'] = []
|
||||||
|
prepared['columns'] = []
|
||||||
|
prepared['row_count'] = 0
|
||||||
|
prepared['plot_data'] = None
|
||||||
|
prepared['error'] = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
results = self.execute(widget['query']) or []
|
||||||
|
columns = list(results[0].keys()) if results else []
|
||||||
|
prepared['row_count'] = len(results)
|
||||||
|
prepared['results'] = results[:100]
|
||||||
|
prepared['columns'] = columns
|
||||||
|
|
||||||
|
if widget['visualization_type'] == 'graph' and results:
|
||||||
|
prepared['plot_data'] = prepare_svg_plot_data(
|
||||||
|
results[:100], columns, widget['title']
|
||||||
|
)
|
||||||
|
except Exception as error:
|
||||||
|
# A broken saved query should not take down the Home page.
|
||||||
|
try:
|
||||||
|
self.execute('ROLLBACK')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
prepared['error'] = str(error)
|
||||||
|
|
||||||
|
prepared_widgets.append(prepared)
|
||||||
|
|
||||||
|
return prepared_widgets
|
||||||
|
|
||||||
def get_people_ids(self):
|
def get_people_ids(self):
|
||||||
query = """
|
query = """
|
||||||
SELECT person_id
|
SELECT person_id
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
-- Saved SQL insights can be pinned as live widgets on the home dashboard.
|
||||||
|
-- Keep the existing saved_query table compatible with older installations.
|
||||||
|
CREATE TABLE IF NOT EXISTS saved_query (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
query TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS dashboard_widget (
|
||||||
|
widget_id SERIAL PRIMARY KEY,
|
||||||
|
person_id INTEGER NOT NULL REFERENCES person(person_id) ON DELETE CASCADE,
|
||||||
|
saved_query_id INTEGER NOT NULL REFERENCES saved_query(id) ON DELETE CASCADE,
|
||||||
|
visualization_type VARCHAR(20) NOT NULL DEFAULT 'table',
|
||||||
|
position INTEGER NOT NULL DEFAULT 0,
|
||||||
|
width INTEGER NOT NULL DEFAULT 1,
|
||||||
|
is_visible BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT dashboard_widget_visualization_type_chk
|
||||||
|
CHECK (visualization_type IN ('table', 'graph')),
|
||||||
|
CONSTRAINT dashboard_widget_unique_view
|
||||||
|
UNIQUE (person_id, saved_query_id, visualization_type)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS dashboard_widget_person_position_idx
|
||||||
|
ON dashboard_widget (person_id, position, widget_id);
|
||||||
+58
-1
@@ -67,7 +67,29 @@ def _save_query(title, query):
|
|||||||
|
|
||||||
def _list_saved_queries():
|
def _list_saved_queries():
|
||||||
"""Lists all saved queries."""
|
"""Lists all saved queries."""
|
||||||
return db.execute("SELECT id, title, query FROM saved_query ORDER BY title")
|
person_id = getattr(current_user, 'id', None) or -1
|
||||||
|
return db.execute("""
|
||||||
|
SELECT
|
||||||
|
q.id,
|
||||||
|
q.title,
|
||||||
|
q.query,
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM dashboard_widget w
|
||||||
|
WHERE w.saved_query_id = q.id
|
||||||
|
AND w.person_id = %s
|
||||||
|
AND w.visualization_type = 'table'
|
||||||
|
AND w.is_visible = TRUE
|
||||||
|
) AS table_pinned,
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM dashboard_widget w
|
||||||
|
WHERE w.saved_query_id = q.id
|
||||||
|
AND w.person_id = %s
|
||||||
|
AND w.visualization_type = 'graph'
|
||||||
|
AND w.is_visible = TRUE
|
||||||
|
) AS graph_pinned
|
||||||
|
FROM saved_query q
|
||||||
|
ORDER BY q.title
|
||||||
|
""", [person_id, person_id])
|
||||||
|
|
||||||
def _get_saved_query(query_id):
|
def _get_saved_query(query_id):
|
||||||
"""Fetches a specific saved query."""
|
"""Fetches a specific saved query."""
|
||||||
@@ -78,6 +100,41 @@ def _delete_saved_query(query_id):
|
|||||||
"""Deletes a saved query."""
|
"""Deletes a saved query."""
|
||||||
db.execute("DELETE FROM saved_query WHERE id=%s", [query_id], commit=True)
|
db.execute("DELETE FROM saved_query WHERE id=%s", [query_id], commit=True)
|
||||||
|
|
||||||
|
|
||||||
|
@sql_explorer_bp.route('/widget/<int:query_id>', methods=['POST', 'DELETE'])
|
||||||
|
@login_required
|
||||||
|
def dashboard_widget(query_id):
|
||||||
|
"""Pin or unpin a saved query as a personal Home dashboard widget."""
|
||||||
|
visualization_type = request.values.get('visualization_type', 'table')
|
||||||
|
if visualization_type not in ('table', 'graph'):
|
||||||
|
return "Unsupported visualization type", 400
|
||||||
|
|
||||||
|
query = db.execute("SELECT id FROM saved_query WHERE id=%s", [query_id], one=True)
|
||||||
|
if not query:
|
||||||
|
return "Query not found", 404
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
db.execute("""
|
||||||
|
INSERT INTO dashboard_widget (person_id, saved_query_id, visualization_type)
|
||||||
|
VALUES (%s, %s, %s)
|
||||||
|
ON CONFLICT (person_id, saved_query_id, visualization_type)
|
||||||
|
DO UPDATE SET is_visible = TRUE
|
||||||
|
""", [current_user.id, query_id, visualization_type], commit=True)
|
||||||
|
else:
|
||||||
|
db.execute("""
|
||||||
|
DELETE FROM dashboard_widget
|
||||||
|
WHERE person_id=%s AND saved_query_id=%s AND visualization_type=%s
|
||||||
|
""", [current_user.id, query_id, visualization_type], commit=True)
|
||||||
|
|
||||||
|
# Home widgets target themselves, while SQL Explorer refreshes its library.
|
||||||
|
if request.headers.get('HX-Target', '').startswith('dashboard-widget-'):
|
||||||
|
return ''
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'partials/sql_explorer/saved_queries.html',
|
||||||
|
saved_queries=_list_saved_queries()
|
||||||
|
)
|
||||||
|
|
||||||
def _generate_sql_from_natural_language(natural_query):
|
def _generate_sql_from_natural_language(natural_query):
|
||||||
"""Generates SQL query from natural language using Gemini REST API."""
|
"""Generates SQL query from natural language using Gemini REST API."""
|
||||||
gemni_model = os.environ.get("GEMINI_MODEL","gemini-2.5-flash-lite")
|
gemni_model = os.environ.get("GEMINI_MODEL","gemini-2.5-flash-lite")
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ Execute SQL migration files using the Flask app's database connection
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Add parent directory to path to import app
|
# Add the project root to the import path when this file is invoked directly.
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
sys.path.insert(0, PROJECT_ROOT)
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv(os.path.join(PROJECT_ROOT, '.env'))
|
||||||
|
|
||||||
from extensions import db as database
|
from extensions import db as database
|
||||||
|
|
||||||
@@ -14,7 +18,7 @@ def run_migration(migration_file):
|
|||||||
"""Execute a SQL migration file"""
|
"""Execute a SQL migration file"""
|
||||||
try:
|
try:
|
||||||
# Read migration file
|
# Read migration file
|
||||||
migration_path = os.path.join('migrations', migration_file)
|
migration_path = os.path.join(PROJECT_ROOT, 'migrations', migration_file)
|
||||||
if not os.path.exists(migration_path):
|
if not os.path.exists(migration_path):
|
||||||
print(f"ERROR: Migration file not found: {migration_path}")
|
print(f"ERROR: Migration file not found: {migration_path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -94,6 +94,23 @@
|
|||||||
hx-target="this" hx-swap="outerHTML">
|
hx-target="this" hx-swap="outerHTML">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if pinned_widgets %}
|
||||||
|
<section class="mb-8">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold text-gray-900">Pinned insights</h2>
|
||||||
|
<p class="text-sm text-gray-500">Live tables and graphs from your saved SQL queries.</p>
|
||||||
|
</div>
|
||||||
|
<a hx-get="{{ url_for('sql_explorer.sql_explorer') }}" hx-push-url="true" hx-target="#container" class="text-sm font-medium text-cyan-600 hover:text-cyan-800 cursor-pointer">Manage insights</a>
|
||||||
|
</div>
|
||||||
|
<div class="w-full grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||||
|
{% for widget in pinned_widgets %}
|
||||||
|
{{ render_partial('partials/dashboard_insight.html', widget=widget) }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if dashboard|length == 0 %}
|
{% if dashboard|length == 0 %}
|
||||||
<div class="bg-purple-100 rounded-lg py-5 px-6 mb-4 text-base text-purple-700 mb-3" role="alert" id="no-workouts">
|
<div class="bg-purple-100 rounded-lg py-5 px-6 mb-4 text-base text-purple-700 mb-3" role="alert" id="no-workouts">
|
||||||
No workouts selected.
|
No workouts selected.
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<article id="dashboard-widget-{{ widget.widget_id }}" class="bg-white shadow rounded-lg p-4 sm:p-6 border border-gray-100">
|
||||||
|
<div class="flex items-start justify-between gap-3 mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-bold text-gray-900">{{ widget.title }}</h3>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Live SQL insight · {{ widget.row_count }} rows</p>
|
||||||
|
</div>
|
||||||
|
<button hx-delete="{{ url_for('sql_explorer.dashboard_widget', query_id=widget.query_id) }}" hx-vals='{"visualization_type":"{{ widget.visualization_type }}"}' hx-target="#dashboard-widget-{{ widget.widget_id }}" hx-swap="outerHTML" hx-confirm="Unpin '{{ widget.title }}' from Home?" class="text-gray-400 hover:text-red-600 p-1 rounded-lg hover:bg-red-50" title="Unpin insight">
|
||||||
|
<span class="sr-only">Unpin insight</span>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% if widget.error %}
|
||||||
|
<div class="p-3 text-sm text-red-700 bg-red-50 border border-red-200 rounded-lg">This insight could not be refreshed: {{ widget.error }}</div>
|
||||||
|
{% elif widget.visualization_type == 'graph' and widget.plot_data %}
|
||||||
|
{{ render_partial('partials/sql_explorer/svg_plot.html', show_title=False, **widget.plot_data) }}
|
||||||
|
{% elif widget.results %}
|
||||||
|
<div class="overflow-x-auto max-h-96">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200 text-sm">
|
||||||
|
<thead class="bg-gray-50"><tr>{% for col in widget.columns %}<th class="px-3 py-2 text-left text-xs font-semibold text-gray-500 uppercase">{{ col }}</th>{% endfor %}</tr></thead>
|
||||||
|
<tbody class="divide-y divide-gray-100">
|
||||||
|
{% for row in widget.results %}<tr>{% for col in widget.columns %}<td class="px-3 py-2 whitespace-nowrap text-gray-700">{{ row[col] if row[col] is not none else 'NULL' }}</td>{% endfor %}</tr>{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-sm text-gray-500 py-4">No data returned by this query.</p>
|
||||||
|
{% endif %}
|
||||||
|
</article>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<div id="saved-queries-library" class="pt-10 border-t border-gray-100">
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-bold text-gray-800">Saved Queries Library</h2>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">Pin a table or graph to your Home dashboard.</p>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs font-medium text-gray-400 uppercase tracking-widest">{{ saved_queries|length }} Queries Total</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if saved_queries %}
|
||||||
|
<div class="bg-white border border-gray-200 rounded-2xl overflow-hidden shadow-sm">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full table-zebra">
|
||||||
|
<thead class="bg-gray-50/50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider border-b">Query Title</th>
|
||||||
|
<th class="px-6 py-4 text-right text-xs font-bold text-gray-500 uppercase tracking-wider border-b">Quick Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-100">
|
||||||
|
{% for saved in saved_queries %}
|
||||||
|
<tr class="group transition-colors">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<button hx-get="{{ url_for('sql_explorer.load_sql_query', query_id=saved.id) }}" hx-target="#sql-query" class="flex items-center text-sm font-medium text-gray-900 hover:text-blue-600 group-hover:translate-x-1 transition-all">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2.5 text-gray-400 group-hover:text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 3.057 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-3.057-9.542-7z" /></svg>
|
||||||
|
{{ saved.title or 'Untitled Query' }}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div class="flex items-center justify-end gap-2">
|
||||||
|
<button hx-get="{{ url_for('sql_explorer.plot_query', query_id=saved.id) }}" hx-target="#sql-plot-results" class="text-green-600 hover:text-green-800 px-2 py-1 rounded-lg hover:bg-green-50 transition-colors text-sm" title="Visualize plot">Preview graph</button>
|
||||||
|
{% if saved.table_pinned %}
|
||||||
|
<button hx-delete="{{ url_for('sql_explorer.dashboard_widget', query_id=saved.id) }}" hx-vals='{"visualization_type":"table"}' hx-target="#saved-queries-library" hx-swap="outerHTML" class="text-blue-700 bg-blue-50 hover:bg-blue-100 px-2 py-1 rounded-lg transition-colors text-sm" title="Unpin table">Table pinned</button>
|
||||||
|
{% else %}
|
||||||
|
<button hx-post="{{ url_for('sql_explorer.dashboard_widget', query_id=saved.id) }}" hx-vals='{"visualization_type":"table"}' hx-target="#saved-queries-library" hx-swap="outerHTML" class="text-gray-600 hover:text-blue-700 px-2 py-1 rounded-lg hover:bg-blue-50 transition-colors text-sm" title="Pin table to Home">Pin table</button>
|
||||||
|
{% endif %}
|
||||||
|
{% if saved.graph_pinned %}
|
||||||
|
<button hx-delete="{{ url_for('sql_explorer.dashboard_widget', query_id=saved.id) }}" hx-vals='{"visualization_type":"graph"}' hx-target="#saved-queries-library" hx-swap="outerHTML" class="text-purple-700 bg-purple-50 hover:bg-purple-100 px-2 py-1 rounded-lg transition-colors text-sm" title="Unpin graph">Graph pinned</button>
|
||||||
|
{% else %}
|
||||||
|
<button hx-post="{{ url_for('sql_explorer.dashboard_widget', query_id=saved.id) }}" hx-vals='{"visualization_type":"graph"}' hx-target="#saved-queries-library" hx-swap="outerHTML" class="text-gray-600 hover:text-purple-700 px-2 py-1 rounded-lg hover:bg-purple-50 transition-colors text-sm" title="Pin graph to Home">Pin graph</button>
|
||||||
|
{% endif %}
|
||||||
|
<button hx-delete="{{ url_for('sql_explorer.delete_sql_query', query_id=saved.id) }}" hx-target="#sql-query" hx-confirm="Delete query '{{ saved.title }}'?" class="text-red-400 hover:text-red-600 p-1 rounded-lg hover:bg-red-50 transition-colors" title="Delete query">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5-4h4a2 2 0 012 2v1H7V5a2 2 0 012-2z" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-12 bg-gray-50 rounded-2xl border-2 border-dashed border-gray-200">
|
||||||
|
<h3 class="mt-2 text-sm font-medium text-gray-900">No saved queries</h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">Create and save a query to pin it to Home.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
@@ -140,7 +140,10 @@
|
|||||||
<!-- Plot Results -->
|
<!-- Plot Results -->
|
||||||
<div id="sql-plot-results" class="transition-all duration-300"></div>
|
<div id="sql-plot-results" class="transition-all duration-300"></div>
|
||||||
|
|
||||||
<!-- Saved Queries Section -->
|
{{ render_partial('partials/sql_explorer/saved_queries.html', saved_queries=saved_queries) }}
|
||||||
|
|
||||||
|
<!-- Legacy Saved Queries Section (kept hidden for compatibility while the partial is adopted) -->
|
||||||
|
<div class="hidden">
|
||||||
<div class="pt-10 border-t border-gray-100">
|
<div class="pt-10 border-t border-gray-100">
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<h2 class="text-xl font-bold text-gray-800">Saved Queries Library</h2>
|
<h2 class="text-xl font-bold text-gray-800">Saved Queries Library</h2>
|
||||||
@@ -221,4 +224,5 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
{% set unique_id = range(1000, 9999) | random %} {# Simple unique ID for elements #}
|
{% set unique_id = range(1000, 9999) | random %} {# Simple unique ID for elements #}
|
||||||
|
|
||||||
<div class="sql-plot-container p-4 border rounded bg-white shadow" id="sql-plot-{{ unique_id }}">
|
<div class="sql-plot-container p-4 border rounded bg-white shadow" id="sql-plot-{{ unique_id }}">
|
||||||
<h4 class="text-lg font-semibold text-gray-700 text-center mb-2">{{ title }}</h4>
|
{% if show_title|default(true) %}<h4 class="text-lg font-semibold text-gray-700 text-center mb-2">{{ title }}</h4>{% endif %}
|
||||||
|
|
||||||
{% if plot_type == 'table' %}
|
{% if plot_type == 'table' %}
|
||||||
{# Fallback to rendering a table if plot type is not supported or data is unsuitable #}
|
{# Fallback to rendering a table if plot type is not supported or data is unsuitable #}
|
||||||
|
|||||||
Reference in New Issue
Block a user