diff --git a/server/tailsql/static/script.js b/server/tailsql/static/script.js index 13cd126..188c7a6 100644 --- a/server/tailsql/static/script.js +++ b/server/tailsql/static/script.js @@ -1,19 +1,27 @@ import { Params, Area, Cycle, Loop } from './sprite.js'; (() => { - const query = document.getElementById('query'); - const qButton = document.getElementById('send-query'); - const dlButton = document.getElementById("dl-button"); - const qform = document.getElementById('qform'); - const output = document.getElementById('output'); - const base = document.location.origin + document.location.pathname; - const sources = document.getElementById('sources'); - const body = document.getElementById('tsql'); + const query = document.getElementById('query'); + const qButton = document.getElementById('send-query'); + const dlButton = document.getElementById("dl-button"); + const saveButton = document.getElementById('save-query'); + const qform = document.getElementById('qform'); + const output = document.getElementById('output'); + const base = document.location.origin + document.location.pathname; + const sources = document.getElementById('sources'); + const body = document.getElementById('tsql'); + const historyList = document.getElementById('history-list'); + const clearHistory = document.getElementById('clear-history'); + const savedList = document.getElementById('saved-query-list'); const nuts = /a?corn|\bnut\b|seed|squirrel|tailsql/i; const velo = 5, delay = 60, runChance = 0.03; let hasRun = false; + const LOCALSTORAGE_KEY_HISTORY = 'tailsql-history'; + const LOCALSTORAGE_KEY_SAVED = 'tailsql-saved'; + const MAX_HISTORY_ENTRIES = 20; + const param = new Params(256, 256, 8, 8); const aRunRight = new Loop(velo, 0, 5, [5,1,2,3]); const aRunLeft = new Loop(-velo, 0, 6, [5,1,2,3]); @@ -105,10 +113,131 @@ import { Params, Area, Cycle, Loop } from './sprite.js'; performDownload('query.csv', href); }); + function getHistory() { + try { return JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_HISTORY)) || []; } + catch { return []; } + } + + function setHistory(h) { + localStorage.setItem(LOCALSTORAGE_KEY_HISTORY, JSON.stringify(h)); + } + + function getSavedQueries() { + try { return JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY_SAVED)) || []; } + catch { return []; } + } + + function setSavedQueries(s) { + localStorage.setItem(LOCALSTORAGE_KEY_SAVED, JSON.stringify(s)); + } + + function renderHistory() { + historyList.innerHTML = ''; + const h = getHistory(); + for (const entry of h) { + const li = document.createElement('li'); + const span = document.createElement('span'); + span.className = 'query-text'; + span.textContent = entry.query; + span.title = entry.query; + li.appendChild(span); + li.addEventListener('click', () => { + query.value = entry.query; + if (sources && entry.source) { + for (const opt of sources.options) { + if (opt.value === entry.source) { + opt.selected = true; + break; + } + } + } + query.focus(); + }); + historyList.appendChild(li); + } + } + + function renderSavedQueries() { + savedList.innerHTML = ''; + const s = getSavedQueries(); + for (let i = 0; i < s.length; i++) { + const entry = s[i]; + const li = document.createElement('li'); + const name = document.createElement('span'); + name.className = 'query-name'; + name.textContent = entry.name + ':'; + const span = document.createElement('span'); + span.className = 'query-text'; + span.textContent = entry.query; + span.title = entry.query; + const del = document.createElement('button'); + del.className = 'delete-btn'; + del.textContent = '\u00d7'; + del.title = 'Delete saved query'; + del.addEventListener('click', (evt) => { + evt.stopPropagation(); + const cur = getSavedQueries(); + cur.splice(i, 1); + setSavedQueries(cur); + renderSavedQueries(); + }); + li.appendChild(name); + li.appendChild(span); + li.appendChild(del); + li.addEventListener('click', () => { + query.value = entry.query; + query.focus(); + }); + savedList.appendChild(li); + } + } + + function recordHistory() { + const errorDiv = document.getElementById('error'); + const q = query.value.trim(); + if (!q || errorDiv || !output) return; + + const h = getHistory().filter(e => e.query !== q); + h.unshift({ query: q, source: sources.value, ts: Date.now() }); + if (h.length > MAX_HISTORY_ENTRIES) h.length = MAX_HISTORY_ENTRIES; + setHistory(h); + } + + saveButton.addEventListener('click', (evt) => { + evt.preventDefault(); + const q = query.value.trim(); + if (!q) return; + const name = prompt('Name for this query:'); + if (!name) return; + const s = getSavedQueries(); + s.push({ query: q, name: name.trim() }); + setSavedQueries(s); + renderSavedQueries(); + }); + + clearHistory.addEventListener('click', (evt) => { + evt.stopPropagation(); + localStorage.removeItem(LOCALSTORAGE_KEY_HISTORY); + renderHistory(); + }); + // Disable the download button when there are no query results. window.addEventListener("load", disableIfNoOutput(dlButton)); // Initially focus the query input. window.addEventListener("load", (evt) => { query.focus(); }); // Refresh when the input source changes. sources.addEventListener('change', (evt) => { qform.submit() }); + + // Persist open/closed state of query panels across page reloads. + document.querySelectorAll('#query-panels details').forEach((d) => { + if (localStorage.getItem(d.id) === 'open') d.open = true; + d.addEventListener('toggle', () => { + if (d.open) localStorage.setItem(d.id, 'open'); + else localStorage.removeItem(d.id); + }); + }); + + recordHistory(); + renderHistory(); + renderSavedQueries(); })() diff --git a/server/tailsql/static/style.css b/server/tailsql/static/style.css index f893a4e..c526f99 100644 --- a/server/tailsql/static/style.css +++ b/server/tailsql/static/style.css @@ -190,6 +190,70 @@ div.action { display: none; } +#query-panels { + display: flex; + align-items: flex-start; + gap: 1rem; + padding: 0.5rem 0; +} +#query-panels details { + flex: 1; + border: 1px solid rgba(27, 31, 35, 0.15); + border-radius: 6px; + overflow: hidden; +} +#query-panels summary { + font-size: 80%; + font-weight: 700; + padding: 0.5rem 0.75rem; + background: var(--bg-colname); + border-bottom: 1px solid rgba(27, 31, 35, 0.15); +} +#query-panels ul { + list-style: none; + margin: 0; + padding: 0; + max-height: 12rem; + overflow-y: auto; +} +#query-panels li { + font-family: "andale mono", verdana, monospace; + font-size: 60%; + padding: 0.4rem 0.75rem; + cursor: pointer; + display: flex; + gap: 0.5rem; + border-bottom: 1px solid rgba(27, 31, 35, 0.06); +} +#query-panels li:last-child { + border-bottom: none; +} +#query-panels li:hover { + background: var(--bg-colname); +} +#query-panels li .query-text { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +#query-panels li .delete-btn { + background: none; + border: none; + cursor: pointer; + color: var(--text-light); + padding: 0 0.2rem; +} +#query-panels li .query-name { + font-family: var(--body-font); + font-weight: 600; +} +.clear-link { + font-weight: normal; + font-size: 90%; + color: var(--text-light); + cursor: pointer; +} #nut { transform: scale(1.5); position: relative; diff --git a/server/tailsql/ui.tmpl b/server/tailsql/ui.tmpl index 27ecbfc..12b0785 100644 --- a/server/tailsql/ui.tmpl +++ b/server/tailsql/ui.tmpl @@ -21,6 +21,7 @@