Implement basic history filtering UI

This commit is contained in:
Simon Cordt
2025-09-24 22:48:20 +02:00
parent 731717c8e1
commit 67c8b603bc
2 changed files with 152 additions and 2 deletions
+5
View File
@@ -109,6 +109,11 @@
.pqueue-thumb-row{display:flex;flex-wrap:wrap;gap:.5rem}
.pqueue-thumb-wrap{position:relative;width:100px;height:100px;border-radius:8px;overflow:hidden;background:rgba(24,24,27,.6);border:1px solid rgba(82,82,91,.24);cursor:pointer}
.pqueue-history-sentinel{display:flex;align-items:center;justify-content:center;min-height:48px;opacity:.7}
.pqueue-popover{position:fixed;min-width:280px;max-width:420px;max-height:80vh;overflow:auto;padding:.85rem;background:#1a1a1a;border:1px solid rgba(82,82,91,.28);border-radius:12px;box-shadow:0 18px 40px rgba(0,0,0,.45);z-index:100000}
.pqueue-popover__section{display:flex;gap:.5rem;flex-wrap:wrap;margin-bottom:.6rem}
.pqueue-popover__grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.5rem}
.pqueue-popover .pqueue-input{width:100%}
@media(max-width:520px){.pqueue-popover{max-width:92vw}.pqueue-popover__grid{grid-template-columns:1fr}}
.pqueue-thumb{width:100%;height:100%;object-fit:cover;display:block}
.pqueue-thumb-badge{position:absolute;right:6px;bottom:6px;padding:.15rem .5rem;border-radius:.6rem;background:rgba(15,23,42,.75);color:#fff;font-size:.68rem}
.pqueue-thumb-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;color:var(--subtle-text,#94a3b8);height:100%;padding:.5rem;font-size:.75rem;gap:.35rem}
+147 -2
View File
@@ -142,6 +142,10 @@
selectedPending: new Set(),
filters: {
pending: "",
historySince: "",
historyUntil: "",
historySinceTime: "",
historyUntilTime: "",
},
historyIds: new Set(),
historyPaging: {
@@ -705,11 +709,117 @@
const sentinel = UI.historySentinel();
state.dom.historySentinel = sentinel;
grid.appendChild(sentinel);
return UI.card("Recent history", {
const base = state.metrics.historyCount ? `${state.metrics.historyCount} entries` : null;
const range = UI.currentHistoryRangeLabel();
const subtitle = [base, range].filter(Boolean).join(' • ');
const card = UI.card("Recent history", {
icon: "ti ti-clock-bolt",
subtitle: state.metrics.historyCount ? `${state.metrics.historyCount} entries` : null,
subtitle,
actions: UI.historyFilters(),
content: grid,
});
state.dom.historyCard = card;
state.dom.historySubtitle = card.querySelector('.pqueue-card__subtitle');
return card;
},
currentHistoryRangeLabel() {
try {
const sDate = state.filters?.historySince || "";
const sTime = state.filters?.historySinceTime || "";
const uDate = state.filters?.historyUntil || "";
const uTime = state.filters?.historyUntilTime || "";
if (!sDate && !uDate) return "";
if (state.historyPreset === 'today') return 'Today';
if (state.historyPreset === '24h') return 'Last 24h';
if (state.historyPreset === '7d') return 'Last 7 days';
const compact = (d, t, end) => {
if (!d) return '';
const parts = d.split('-').map((x) => parseInt(x, 10));
let hh = end ? 23 : 0, mm = end ? 59 : 0;
if (t && /^\d{2}:\d{2}/.test(t)) {
const tt = t.split(':').map((x) => parseInt(x, 10));
hh = tt[0] ?? hh; mm = tt[1] ?? mm;
}
const pad = (n) => String(n).padStart(2, '0');
return `${parts[0]}-${pad(parts[1])}-${pad(parts[2])} ${pad(hh)}:${pad(mm)}`;
};
const left = compact(sDate, sTime, false);
const right = compact(uDate, uTime, true);
if (left && right) return `${left} – ${right}`;
if (left) return `Since ${left}`;
if (right) return `Until ${right}`;
return "";
} catch (err) {
return "";
}
},
updateHistorySubtitle() {
try {
const el = state.dom.historySubtitle;
if (!el) return;
const base = state.metrics?.historyCount ? `${state.metrics.historyCount} entries` : null;
const range = UI.currentHistoryRangeLabel();
const text = [base, range].filter(Boolean).join(' • ');
el.textContent = text;
} catch (err) {
/* noop */
}
},
historyFilters() {
const filtersBtn = UI.button({ icon: "ti ti-filter", variant: "ghost", subtle: true, title: "Filters" });
const sortToggle = UI.sortToggleButton();
filtersBtn.addEventListener('click', (e) => {
e.stopPropagation();
Events.toggleFiltersPopover(e.currentTarget);
});
document.addEventListener('click', (ev) => {
const pop = state.dom.filtersPopover;
if (pop && !pop.contains(ev.target) && ev.target !== filtersBtn) {
pop.remove();
state.dom.filtersPopover = null;
}
}, { once: true });
return [filtersBtn, sortToggle, UI.clearFiltersIcon()];
},
clearFiltersIcon() {
const btn = UI.button({ icon: "ti ti-x", variant: "ghost", subtle: true, title: "Clear filters" });
btn.addEventListener('click', () => Events.applyHistoryPreset('clear'));
return btn;
},
sortToggleButton() {
const dir = (state.historyPaging?.params?.sort_dir === 'asc') ? 'asc' : 'desc';
const icon = dir === 'asc' ? 'ti ti-arrow-bar-to-up' : 'ti ti-arrow-bar-to-down';
const title = dir === 'asc' ? 'Sort ascending (click to toggle)' : 'Sort descending (click to toggle)';
const btn = UI.button({ icon, variant: 'ghost', subtle: true, title });
btn.addEventListener('click', () => {
const current = (state.historyPaging?.params?.sort_dir === 'asc') ? 'asc' : 'desc';
const next = current === 'asc' ? 'desc' : 'asc';
Events.setHistorySort(next);
UI.updateSortToggle();
});
state.dom.sortToggleBtn = btn;
return btn;
},
updateSortToggle() {
try {
const btn = state.dom.sortToggleBtn;
if (!btn) return;
const dir = (state.historyPaging?.params?.sort_dir === 'asc') ? 'asc' : 'desc';
const icon = dir === 'asc' ? 'ti ti-arrow-bar-to-up' : 'ti ti-arrow-bar-to-down';
btn.innerHTML = '';
btn.appendChild(UI.icon(icon));
btn.title = dir === 'asc' ? 'Sort ascending (click to toggle)' : 'Sort descending (click to toggle)';
} catch (err) {
/* noop */
}
},
historySentinel() {
@@ -856,6 +966,41 @@
}
},
buildFiltersPopover(anchor) {
const since = UI.el("input", { type: "date", class: "pqueue-input", value: state.filters?.historySince || "" });
const sinceTime = UI.el("input", { type: "time", class: "pqueue-input", value: state.filters?.historySinceTime || "" });
const until = UI.el("input", { type: "date", class: "pqueue-input", value: state.filters?.historyUntil || "" });
const untilTime = UI.el("input", { type: "time", class: "pqueue-input", value: state.filters?.historyUntilTime || "" });
const onChange = () => Events.updateHistoryDateFilters(since.value, until.value, sinceTime.value, untilTime.value);
since.addEventListener("change", onChange);
sinceTime.addEventListener("change", onChange);
until.addEventListener("change", onChange);
untilTime.addEventListener("change", onChange);
const presets = UI.el("div", { class: "pqueue-popover__section" }, [
UI.button({ text: "Today", variant: "ghost", subtle: true, size: "sm", onClick: () => Events.applyHistoryPreset("today") }),
UI.button({ text: "Last 24h", variant: "ghost", subtle: true, size: "sm", onClick: () => Events.applyHistoryPreset("24h") }),
UI.button({ text: "Last 7d", variant: "ghost", subtle: true, size: "sm", onClick: () => Events.applyHistoryPreset("7d") }),
UI.button({ text: "Clear", variant: "ghost", subtle: true, size: "sm", onClick: () => Events.applyHistoryPreset("clear") }),
]);
const dates = UI.el("div", { class: "pqueue-popover__grid" }, [since, sinceTime, until, untilTime]);
const pop = UI.el('div', { class: 'pqueue-popover' }, [
UI.el('div', { class: 'pqueue-popover__section' }, [UI.el('strong', { text: 'Date range' })]),
dates,
UI.el('div', { class: 'pqueue-popover__section' }, [UI.el('strong', { text: 'Presets' })]),
presets
]);
// Position near anchor
try {
const rect = anchor.getBoundingClientRect();
pop.style.top = `${rect.bottom + 8 + window.scrollY}px`;
pop.style.left = `${Math.min(rect.left + window.scrollX, window.innerWidth - 360)}px`;
} catch (err) {}
return pop;
},
refreshFilter() {
const table = state.dom.pendingTable;
if (!table) return;