feat(dev_tools): enhance SQL Query Tool — clear, copy, history, edit, hardening
All checks were successful
CI / ruff (push) Successful in 15s
CI / pytest (push) Successful in 2h42m26s
CI / validate (push) Successful in 34s
CI / dependency-scanning (push) Successful in 34s
CI / docs (push) Successful in 54s
CI / deploy (push) Successful in 1m43s

UI: add Clear and Copy-to-clipboard (TSV) buttons, an in-page Recent
Queries pane (localStorage, capped at 20, de-duped) and a pencil-edit
flow for saved queries with a dedicated SQL field in the modal.
Bind Ctrl/Cmd+S to open the save modal (or edit the active saved query).

Backend: harden validate_query with a multi-statement guard that
respects string literals + comments. Stop swallowing record_query_run
errors silently — log via logger.exception so failures show up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 11:24:40 +02:00
parent 64a178f45d
commit e94b6d07bb
5 changed files with 268 additions and 18 deletions

View File

@@ -79,11 +79,47 @@
Run <span x-text="q.run_count"></span> time<span x-show="q.run_count !== 1">s</span>
</div>
</div>
<button @click.stop="deleteSavedQuery(q.id)"
class="opacity-0 group-hover:opacity-100 p-1 text-gray-400 hover:text-red-500 transition-opacity"
title="Delete">
<span x-html="$icon('trash', 'w-4 h-4')"></span>
</button>
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button @click.stop="openEditModal(q)"
class="p-1 text-gray-400 hover:text-indigo-500"
title="Edit">
<span x-html="$icon('pencil', 'w-4 h-4')"></span>
</button>
<button @click.stop="deleteSavedQuery(q.id)"
class="p-1 text-gray-400 hover:text-red-500"
title="Delete">
<span x-html="$icon('trash', 'w-4 h-4')"></span>
</button>
</div>
</li>
</template>
</ul>
</div>
<!-- Recent Queries (history) -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider flex items-center gap-1.5">
<span x-html="$icon('clock', 'w-4 h-4')"></span>
Recent
</h3>
<button @click="clearHistory()"
x-show="history.length > 0"
class="text-xs text-gray-400 hover:text-red-500"
title="Clear history">Clear</button>
</div>
<div x-show="history.length === 0" class="text-sm text-gray-400">No recent queries.</div>
<ul class="space-y-1">
<template x-for="(h, idx) in history" :key="h.ts">
<li @click="loadHistory(h)"
class="rounded-md px-2 py-1.5 text-xs cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors text-gray-600 dark:text-gray-400"
:title="h.sql">
<div class="truncate font-mono" x-text="h.preview"></div>
<div class="text-[10px] text-gray-400 mt-0.5">
<span x-text="formatHistoryTime(h.ts)"></span>
<span x-show="h.row_count !== undefined"> · <span x-text="h.row_count"></span> row<span x-show="h.row_count !== 1">s</span></span>
<span x-show="h.elapsed !== undefined"> · <span x-text="h.elapsed"></span>ms</span>
</div>
</li>
</template>
</ul>
@@ -128,6 +164,22 @@
<span x-html="$icon('download', 'w-4 h-4 mr-1.5')"></span>
Export CSV
</button>
<button @click="copyResults()"
x-show="rows.length > 0"
class="inline-flex items-center px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 text-sm font-medium rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
title="Copy results as TSV (paste into spreadsheet)">
<span x-html="$icon('clipboard', 'w-4 h-4 mr-1.5')"></span>
Copy Results
</button>
<button @click="clearQuery()"
:disabled="!sql && columns.length === 0 && !error"
class="ml-auto inline-flex items-center px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 text-sm font-medium rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
title="Clear editor and results">
<span x-html="$icon('x-circle', 'w-4 h-4 mr-1.5')"></span>
Clear
</button>
</div>
</div>
@@ -182,9 +234,10 @@
</div>
</div>
<!-- Save Query Modal -->
<!-- Save / Edit Query Modal -->
{% call modal('saveQueryModal', 'Save Query', show_var='showSaveModal', size='sm', show_footer=false) %}
<div class="space-y-4">
<div class="text-xs uppercase tracking-wider text-gray-400" x-text="editingSavedId ? 'Edit saved query' : 'New saved query'"></div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Name</label>
<input type="text" x-model="saveName"
@@ -198,6 +251,13 @@
class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 text-sm focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Brief description of what this query does">
</div>
<div x-show="editingSavedId">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">SQL</label>
<textarea x-model="saveSql"
rows="6"
spellcheck="false"
class="w-full bg-gray-900 text-green-400 font-mono text-xs rounded-lg p-3 border border-gray-700 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 resize-y"></textarea>
</div>
<div class="flex justify-end gap-3 pt-2">
<button @click="showSaveModal = false"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors">
@@ -206,7 +266,7 @@
<button @click="saveQuery()"
:disabled="!saveName.trim() || saving"
class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 disabled:opacity-50 transition-colors">
<span x-text="saving ? 'Saving...' : 'Save'"></span>
<span x-text="saving ? 'Saving...' : (editingSavedId ? 'Save changes' : 'Save')"></span>
</button>
</div>
</div>