Add SQL insight dashboard widgets

This commit is contained in:
Peter Stockings
2026-09-06 13:37:46 +10:00
parent ff6a921550
commit d1100ece80
11 changed files with 273 additions and 28 deletions
+19 -2
View File
@@ -94,7 +94,24 @@
hx-target="this" hx-swap="outerHTML">
</div>
{% if dashboard|length == 0 %}
{% 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 %}
<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.
</div>
@@ -193,4 +210,4 @@
hx-target="#stats" hx-swap="innerHTML">
</div>
{% endblock %}
{% endblock %}
+28
View File
@@ -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,8 +140,11 @@
<!-- Plot Results -->
<div id="sql-plot-results" class="transition-all duration-300"></div>
<!-- Saved Queries Section -->
<div class="pt-10 border-t border-gray-100">
{{ 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="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-gray-800">Saved Queries Library</h2>
<span class="text-xs font-medium text-gray-400 uppercase tracking-widest">{{ saved_queries|length }} Queries
@@ -220,5 +223,6 @@
<p class="mt-1 text-sm text-gray-500">Get started by creating and saving your first SQL query.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
@@ -2,7 +2,7 @@
{% 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 }}">
<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' %}
{# Fallback to rendering a table if plot type is not supported or data is unsuitable #}
@@ -122,4 +122,4 @@
</svg>
</div>
{% endif %}
</div>
</div>