feat(pipeline): catalog tab templates

- partials/pipeline_catalog.html: accordion list of serving tables with
  row count badges, column count, click-to-expand lazy-loaded detail
- partials/pipeline_table_detail.html: column schema grid + sticky-header
  sample data table (10 rows, truncated values with title attribute)
- JS: toggleCatalogTable() + htmx.trigger(content, 'revealed') for
  lazy-loading detail only on first open

Subtask 4 of 6

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-25 12:54:21 +01:00
parent 947a1a778e
commit 5b48a11e01
2 changed files with 133 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
<!-- Pipeline Catalog Tab: serving tables with column + sample data expandable -->
{% if serving_meta %}
<p class="text-xs text-slate mb-3">
Serving DB last exported: <span class="mono font-semibold">{{ serving_meta.exported_at_utc[:19].replace('T', ' ') }}</span>
</p>
{% endif %}
{% if tables %}
<div style="display:grid;grid-template-columns:1fr;gap:0.75rem">
{% for table in tables %}
<div class="card" style="padding:0" id="catalog-table-{{ table.name }}">
<!-- Table header row -->
<div class="flex items-center justify-between"
style="padding:0.875rem 1.25rem;cursor:pointer"
onclick="toggleCatalogTable('{{ table.name }}')">
<div class="flex items-center gap-3">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" style="width:16px;height:16px;color:#94A3B8;flex-shrink:0">
<path stroke-linecap="round" stroke-linejoin="round"
d="M3.375 19.5h17.25m-17.25 0a1.125 1.125 0 0 1-1.125-1.125M3.375 19.5h7.5c.621 0 1.125-.504 1.125-1.125m-9.75 0V5.625m0 12.75v-1.5c0-.621.504-1.125 1.125-1.125m18.375 2.625V5.625m0 12.75c0 .621-.504 1.125-1.125 1.125m1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125m0 3.75h-7.5A1.125 1.125 0 0 1 12 18.375m9.75-12.75c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125m19.5 0v1.5c0 .621-.504 1.125-1.125 1.125M2.25 5.625v1.5c0 .621.504 1.125 1.125 1.125m0 0h17.25m-17.25 0h7.5c.621 0 1.125.504 1.125 1.125M3.375 8.25c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375Z"/>
</svg>
<span class="font-semibold text-sm text-navy">serving.<span class="mono">{{ table.name }}</span></span>
<span class="text-xs text-slate">{{ table.column_count }} col{{ 's' if table.column_count != 1 }}</span>
{% if table.row_count is not none %}
<span class="badge" style="background:#F0FDF4;color:#16A34A;font-size:10px">
{{ "{:,}".format(table.row_count) }} rows
</span>
{% endif %}
</div>
<svg id="chevron-{{ table.name }}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor"
style="width:14px;height:14px;color:#94A3B8;transition:transform 0.2s">
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5"/>
</svg>
</div>
<!-- Expandable detail (HTMX lazy-loaded on first open) -->
<div id="catalog-detail-{{ table.name }}" style="display:none;border-top:1px solid #E2E8F0">
<div id="catalog-content-{{ table.name }}"
hx-get="{{ url_for('pipeline.pipeline_table_detail', table_name=table.name) }}"
hx-trigger="revealed"
hx-target="#catalog-content-{{ table.name }}"
hx-swap="outerHTML">
<div style="padding:1.5rem;text-align:center">
<p class="text-sm text-slate">Loading…</p>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="card text-center" style="padding:2rem">
<p class="text-slate">No serving tables found. Run the pipeline to generate them.</p>
</div>
{% endif %}
<script>
function toggleCatalogTable(name) {
var detail = document.getElementById('catalog-detail-' + name);
var chevron = document.getElementById('chevron-' + name);
var isOpen = detail.style.display !== 'none';
detail.style.display = isOpen ? 'none' : 'block';
chevron.style.transform = isOpen ? '' : 'rotate(90deg)';
// Trigger HTMX revealed for lazy-loading when first opened
if (!isOpen) {
var content = document.getElementById('catalog-content-' + name);
if (content && content.getAttribute('hx-trigger') === 'revealed') {
htmx.trigger(content, 'revealed');
}
}
}
</script>

View File

@@ -0,0 +1,59 @@
<!-- Pipeline Table Detail: column schema + 10-row sample data -->
<!-- This partial replaces #catalog-content-<table_name> via HTMX -->
<div id="catalog-content-{{ table_name }}" style="padding:1.25rem">
<!-- Column schema -->
<div class="mb-4">
<p class="text-xs font-semibold text-slate uppercase mb-2" style="letter-spacing:0.05em">Schema</p>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:6px">
{% for col in columns %}
<div style="background:#F8FAFC;border:1px solid #E2E8F0;border-radius:6px;padding:6px 10px">
<span class="text-xs font-semibold text-navy mono">{{ col.column_name }}</span>
<span class="text-xs text-slate ml-2">{{ col.data_type | upper }}</span>
</div>
{% endfor %}
</div>
</div>
<!-- Sample rows -->
{% if sample %}
<div>
<p class="text-xs font-semibold text-slate uppercase mb-2" style="letter-spacing:0.05em">
Sample (first {{ sample | length }} rows)
</p>
<div style="overflow-x:auto;max-height:320px;overflow-y:auto">
<table class="table" style="font-size:0.75rem">
<thead style="position:sticky;top:0;background:#fff;z-index:1">
<tr>
{% for col in columns %}
<th style="white-space:nowrap">{{ col.column_name }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in sample %}
<tr>
{% for col in columns %}
<td class="mono" style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"
title="{{ row[col.column_name] | string }}">
{% set val = row[col.column_name] %}
{% if val is none %}
<span class="text-slate">null</span>
{% elif val | string | length > 40 %}
{{ val | string | truncate(40, true) }}
{% else %}
{{ val }}
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% else %}
<p class="text-sm text-slate">Table is empty.</p>
{% endif %}
</div>