feat(pipeline): query editor tab templates
- partials/pipeline_query.html: dark-themed SQL textarea (navy bg, Commit Mono, 12px border-radius, electric blue focus glow) + schema sidebar (collapsible per-table column lists with types) + controls bar (Execute, Clear, limit/timeout note) + Tab-key indent + Cmd/Ctrl+Enter submit - partials/pipeline_query_results.html: results table with sticky headers, horizontal scroll, row count + elapsed time metadata, truncation warning, error display in red monospace card Subtask 5 of 6 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,258 @@
|
|||||||
|
<!-- Pipeline Query Tab: SQL editor + schema sidebar + results -->
|
||||||
|
<style>
|
||||||
|
.query-layout {
|
||||||
|
display: flex; gap: 1rem; align-items: flex-start;
|
||||||
|
}
|
||||||
|
.query-editor-wrap { flex: 1; min-width: 0; }
|
||||||
|
|
||||||
|
/* Dark code editor textarea */
|
||||||
|
.query-editor {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: 480px;
|
||||||
|
resize: vertical;
|
||||||
|
font-family: 'Commit Mono', 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
tab-size: 2;
|
||||||
|
background: #0F172A;
|
||||||
|
color: #CBD5E1;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1rem 1.125rem;
|
||||||
|
caret-color: #60A5FA;
|
||||||
|
}
|
||||||
|
.query-editor:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3B82F6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(59,130,246,0.18);
|
||||||
|
}
|
||||||
|
.query-editor::placeholder { color: #475569; }
|
||||||
|
|
||||||
|
/* Schema panel */
|
||||||
|
.schema-panel {
|
||||||
|
width: 230px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #F8FAFC;
|
||||||
|
border: 1px solid #E2E8F0;
|
||||||
|
border-radius: 12px;
|
||||||
|
max-height: 480px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
.schema-panel-header {
|
||||||
|
padding: 0.625rem 0.875rem;
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: #94A3B8;
|
||||||
|
border-bottom: 1px solid #E2E8F0;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: #F8FAFC;
|
||||||
|
}
|
||||||
|
.schema-table-section { border-bottom: 1px solid #F1F5F9; }
|
||||||
|
.schema-table-toggle {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.5rem 0.875rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0F172A;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: 'Commit Mono', monospace;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.schema-table-toggle:hover { background: #EFF6FF; color: #1D4ED8; }
|
||||||
|
.schema-cols { display: none; padding: 0 0.875rem 0.5rem; }
|
||||||
|
.schema-cols.open { display: block; }
|
||||||
|
.schema-col {
|
||||||
|
padding: 2px 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.schema-col-name { color: #475569; font-family: 'Commit Mono', monospace; font-size: 0.6875rem; }
|
||||||
|
.schema-col-type { color: #94A3B8; font-size: 0.625rem; white-space: nowrap; }
|
||||||
|
|
||||||
|
/* Query controls bar */
|
||||||
|
.query-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 0.625rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.query-limit-note {
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
color: #94A3B8;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Results area */
|
||||||
|
#query-results { margin-top: 1rem; }
|
||||||
|
.results-meta {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #64748B;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.results-table-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
max-height: 440px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #E2E8F0;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.results-table-wrap table {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.results-table-wrap table thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: #F8FAFC;
|
||||||
|
z-index: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.results-table-wrap table td.mono {
|
||||||
|
max-width: 240px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.query-error {
|
||||||
|
border: 1px solid #FCA5A5;
|
||||||
|
background: #FEF2F2;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.875rem 1rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: #DC2626;
|
||||||
|
font-family: 'Commit Mono', monospace;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.results-truncated {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #D97706;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.query-layout { flex-direction: column; }
|
||||||
|
.schema-panel { width: 100%; max-height: 200px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<form id="query-form"
|
||||||
|
hx-post="{{ url_for('pipeline.pipeline_query_execute') }}"
|
||||||
|
hx-target="#query-results"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-indicator="#query-spinner">
|
||||||
|
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
|
||||||
|
<div class="query-layout">
|
||||||
|
<!-- SQL textarea -->
|
||||||
|
<div class="query-editor-wrap">
|
||||||
|
<textarea
|
||||||
|
name="sql"
|
||||||
|
id="query-sql"
|
||||||
|
class="query-editor"
|
||||||
|
rows="10"
|
||||||
|
spellcheck="false"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
autocapitalize="off"
|
||||||
|
placeholder="-- SELECT * FROM serving.city_market_profile -- WHERE country_code = 'DE' -- ORDER BY marktreife_score DESC -- LIMIT 20"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
<div class="query-controls">
|
||||||
|
<button type="submit" class="btn btn-sm" hx-disabled-elt="this">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
|
||||||
|
stroke="currentColor" style="width:13px;height:13px;display:inline;margin-right:4px">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.347a1.125 1.125 0 0 1 0 1.972l-11.54 6.347a1.125 1.125 0 0 1-1.667-.986V5.653Z"/>
|
||||||
|
</svg>
|
||||||
|
Execute
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn-outline btn-sm" onclick="document.getElementById('query-sql').value='';document.getElementById('query-results').innerHTML=''">
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
<svg id="query-spinner" class="htmx-indicator search-spinner" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
|
||||||
|
</svg>
|
||||||
|
<span class="query-limit-note">
|
||||||
|
{{ max_rows | default(1000) }} row limit · {{ timeout_seconds | default(10) }}s timeout · SELECT only
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Schema sidebar -->
|
||||||
|
<aside class="schema-panel">
|
||||||
|
<div class="schema-panel-header">Schema — serving.*</div>
|
||||||
|
{% if schema %}
|
||||||
|
{% for tname, cols in schema.items() | sort %}
|
||||||
|
<div class="schema-table-section">
|
||||||
|
<button type="button" class="schema-table-toggle" onclick="toggleSchema('{{ tname }}')">
|
||||||
|
{{ tname }}
|
||||||
|
<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;flex-shrink:0">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div id="schema-cols-{{ tname }}" class="schema-cols">
|
||||||
|
{% for col in cols %}
|
||||||
|
<div class="schema-col">
|
||||||
|
<span class="schema-col-name">{{ col.name }}</span>
|
||||||
|
<span class="schema-col-type">{{ col.type | upper }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p style="padding:1rem;font-size:0.75rem;color:#94A3B8">
|
||||||
|
Analytics DB not available.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Results area (populated by HTMX after query execution) -->
|
||||||
|
<div id="query-results"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Tab key in textarea inserts spaces instead of losing focus
|
||||||
|
document.getElementById('query-sql').addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
var start = this.selectionStart;
|
||||||
|
var end = this.selectionEnd;
|
||||||
|
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
|
||||||
|
this.selectionStart = this.selectionEnd = start + 2;
|
||||||
|
}
|
||||||
|
// Cmd/Ctrl+Enter submits
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
htmx.trigger(document.getElementById('query-form'), 'submit');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleSchema(name) {
|
||||||
|
var cols = document.getElementById('schema-cols-' + name);
|
||||||
|
var chevron = document.getElementById('schema-chevron-' + name);
|
||||||
|
var isOpen = cols.classList.contains('open');
|
||||||
|
cols.classList.toggle('open', !isOpen);
|
||||||
|
chevron.style.transform = isOpen ? '' : 'rotate(90deg)';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<!-- Pipeline Query Results Partial: swapped into #query-results after POST -->
|
||||||
|
{% if error %}
|
||||||
|
<div class="query-error">
|
||||||
|
<strong style="display:block;margin-bottom:4px">Query error</strong>
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
{% elif columns %}
|
||||||
|
<div class="results-meta">
|
||||||
|
<span>{{ row_count | default(0) }} row{{ 's' if row_count != 1 }}</span>
|
||||||
|
<span style="color:#CBD5E1">·</span>
|
||||||
|
<span>{{ "%.1f" | format(elapsed_ms) }} ms</span>
|
||||||
|
{% if truncated %}
|
||||||
|
<span class="results-truncated">Result truncated at {{ row_count }} rows</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="results-table-wrap">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{% for col in columns %}
|
||||||
|
<th>{{ col }}</th>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for row in rows %}
|
||||||
|
<tr>
|
||||||
|
{% for val in row %}
|
||||||
|
<td class="mono" title="{{ val if val is not none else 'null' }}">
|
||||||
|
{% if val is none %}
|
||||||
|
<span style="color:#CBD5E1">null</span>
|
||||||
|
{% else %}
|
||||||
|
{{ val | string | truncate(60, true) }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div style="padding:1.5rem;text-align:center">
|
||||||
|
<p class="text-sm text-slate">Query returned no rows.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
Reference in New Issue
Block a user