fix(pipeline): query shortcuts + schema preview + serving meta fallback
- Add Shift+Enter shortcut to execute query (alongside Cmd/Ctrl+Enter) - Add ▶ preview button to schema sidebar tables: populates editor with SELECT * FROM serving.<table> LIMIT 100 and auto-submits - Update hint text to show "Shift+Enter to run" - Overview tab: fall back to information_schema when _serving_meta.json is absent instead of showing error message; row counts show "—" - Dashboard stat cards: same fallback — query DuckDB for table count Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -372,11 +372,22 @@ def _is_stale(run: dict) -> bool:
|
|||||||
@role_required("admin")
|
@role_required("admin")
|
||||||
async def pipeline_dashboard():
|
async def pipeline_dashboard():
|
||||||
"""Main page: health stat cards + tab container."""
|
"""Main page: health stat cards + tab container."""
|
||||||
summary = await asyncio.to_thread(_fetch_extraction_summary_sync)
|
from .analytics import fetch_analytics # noqa: PLC0415
|
||||||
serving_meta = await asyncio.to_thread(_load_serving_meta)
|
|
||||||
|
|
||||||
total_serving_tables = len(serving_meta["tables"]) if serving_meta else 0
|
summary, serving_meta = await asyncio.gather(
|
||||||
last_export = serving_meta.get("exported_at_utc", "")[:19].replace("T", " ") if serving_meta else "—"
|
asyncio.to_thread(_fetch_extraction_summary_sync),
|
||||||
|
asyncio.to_thread(_load_serving_meta),
|
||||||
|
)
|
||||||
|
|
||||||
|
if serving_meta:
|
||||||
|
total_serving_tables = len(serving_meta.get("tables", {}))
|
||||||
|
last_export = serving_meta.get("exported_at_utc", "")[:19].replace("T", " ") or "—"
|
||||||
|
else:
|
||||||
|
schema_rows = await fetch_analytics(
|
||||||
|
"SELECT COUNT(*) AS n FROM information_schema.tables WHERE table_schema = 'serving'"
|
||||||
|
)
|
||||||
|
total_serving_tables = schema_rows[0]["n"] if schema_rows else 0
|
||||||
|
last_export = "—"
|
||||||
|
|
||||||
success_rate = 0
|
success_rate = 0
|
||||||
if summary["total"] > 0:
|
if summary["total"] > 0:
|
||||||
@@ -422,12 +433,30 @@ async def pipeline_overview():
|
|||||||
# Compute landing zone totals
|
# Compute landing zone totals
|
||||||
total_landing_bytes = sum(s["total_bytes"] for s in landing_stats)
|
total_landing_bytes = sum(s["total_bytes"] for s in landing_stats)
|
||||||
|
|
||||||
|
# Build serving tables list: prefer _serving_meta.json (has counts + timestamp),
|
||||||
|
# fall back to information_schema when file doesn't exist yet.
|
||||||
|
if serving_meta:
|
||||||
|
serving_tables = [
|
||||||
|
{"name": name, "row_count": meta.get("row_count")}
|
||||||
|
for name, meta in sorted(serving_meta.get("tables", {}).items())
|
||||||
|
]
|
||||||
|
last_export = serving_meta.get("exported_at_utc", "")[:19].replace("T", " ") or None
|
||||||
|
else:
|
||||||
|
from .analytics import fetch_analytics # noqa: PLC0415
|
||||||
|
schema_rows = await fetch_analytics(
|
||||||
|
"SELECT table_name FROM information_schema.tables "
|
||||||
|
"WHERE table_schema = 'serving' ORDER BY table_name"
|
||||||
|
)
|
||||||
|
serving_tables = [{"name": r["table_name"], "row_count": None} for r in schema_rows]
|
||||||
|
last_export = None
|
||||||
|
|
||||||
return await render_template(
|
return await render_template(
|
||||||
"admin/partials/pipeline_overview.html",
|
"admin/partials/pipeline_overview.html",
|
||||||
workflow_rows=workflow_rows,
|
workflow_rows=workflow_rows,
|
||||||
landing_stats=landing_stats,
|
landing_stats=landing_stats,
|
||||||
total_landing_bytes=total_landing_bytes,
|
total_landing_bytes=total_landing_bytes,
|
||||||
serving_meta=serving_meta,
|
serving_tables=serving_tables,
|
||||||
|
last_export=last_export,
|
||||||
format_bytes=_format_bytes,
|
format_bytes=_format_bytes,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -56,10 +56,12 @@
|
|||||||
<!-- Serving Freshness -->
|
<!-- Serving Freshness -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<p class="card-header">Serving Tables</p>
|
<p class="card-header">Serving Tables</p>
|
||||||
{% if serving_meta %}
|
{% if last_export %}
|
||||||
<p class="text-xs text-slate mb-3">
|
<p class="text-xs text-slate mb-3">
|
||||||
Last export: <span class="mono font-semibold text-navy">{{ serving_meta.exported_at_utc[:19].replace('T', ' ') }}</span>
|
Last export: <span class="mono font-semibold text-navy">{{ last_export }}</span>
|
||||||
</p>
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if serving_tables %}
|
||||||
<table class="table" style="font-size:0.8125rem">
|
<table class="table" style="font-size:0.8125rem">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -68,18 +70,18 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for tname, tmeta in serving_meta.tables.items() | sort %}
|
{% for t in serving_tables %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="mono">serving.{{ tname }}</td>
|
<td class="mono">serving.{{ t.name }}</td>
|
||||||
<td class="mono text-right font-semibold">{{ "{:,}".format(tmeta.row_count) }}</td>
|
<td class="mono text-right font-semibold">
|
||||||
|
{% if t.row_count is not none %}{{ "{:,}".format(t.row_count) }}{% else %}—{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="text-sm text-slate">
|
<p class="text-sm text-slate">No serving tables found — run the pipeline first.</p>
|
||||||
<code>_serving_meta.json</code> not found — run the pipeline to generate it.
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -189,7 +189,7 @@
|
|||||||
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
|
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="query-limit-note">
|
<span class="query-limit-note">
|
||||||
{{ max_rows | default(1000) }} row limit · {{ timeout_seconds | default(10) }}s timeout · SELECT only
|
{{ max_rows | default(1000) }} row limit · {{ timeout_seconds | default(10) }}s timeout · SELECT only · Shift+Enter to run
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -202,12 +202,18 @@
|
|||||||
<div class="schema-table-section">
|
<div class="schema-table-section">
|
||||||
<button type="button" class="schema-table-toggle" onclick="toggleSchema('{{ tname }}')">
|
<button type="button" class="schema-table-toggle" onclick="toggleSchema('{{ tname }}')">
|
||||||
{{ tname }}
|
{{ tname }}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
<span style="display:flex;align-items:center;gap:4px;flex-shrink:0">
|
||||||
stroke-width="2" stroke="currentColor"
|
<span title="Preview table" onclick="event.stopPropagation();previewTable('serving.{{ tname }}')"
|
||||||
id="schema-chevron-{{ tname }}"
|
style="padding:1px 5px;font-size:0.6rem;font-weight:600;background:#DBEAFE;color:#1D4ED8;border-radius:4px;font-family:sans-serif;letter-spacing:0">
|
||||||
style="width:10px;height:10px;transition:transform 0.15s;flex-shrink:0">
|
▶
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5"/>
|
</span>
|
||||||
</svg>
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||||
|
stroke-width="2" stroke="currentColor"
|
||||||
|
id="schema-chevron-{{ tname }}"
|
||||||
|
style="width:10px;height:10px;transition:transform 0.15s">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<div id="schema-cols-{{ tname }}" class="schema-cols">
|
<div id="schema-cols-{{ tname }}" class="schema-cols">
|
||||||
{% for col in cols %}
|
{% for col in cols %}
|
||||||
@@ -241,13 +247,18 @@ document.getElementById('query-sql').addEventListener('keydown', function(e) {
|
|||||||
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
|
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
|
||||||
this.selectionStart = this.selectionEnd = start + 2;
|
this.selectionStart = this.selectionEnd = start + 2;
|
||||||
}
|
}
|
||||||
// Cmd/Ctrl+Enter submits
|
// Shift+Enter or Cmd/Ctrl+Enter submits
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
if ((e.shiftKey || e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
htmx.trigger(document.getElementById('query-form'), 'submit');
|
htmx.trigger(document.getElementById('query-form'), 'submit');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function previewTable(fullName) {
|
||||||
|
document.getElementById('query-sql').value = 'SELECT *\nFROM ' + fullName + '\nLIMIT 100';
|
||||||
|
htmx.trigger(document.getElementById('query-form'), 'submit');
|
||||||
|
}
|
||||||
|
|
||||||
function toggleSchema(name) {
|
function toggleSchema(name) {
|
||||||
var cols = document.getElementById('schema-cols-' + name);
|
var cols = document.getElementById('schema-cols-' + name);
|
||||||
var chevron = document.getElementById('schema-chevron-' + name);
|
var chevron = document.getElementById('schema-chevron-' + name);
|
||||||
|
|||||||
Reference in New Issue
Block a user