UX: unify time selection and simplify controls\n\n- Replace separate Interval + Window with a single Time preset (Last hour/24h/7d/30d/12w/12m/All time)\n- Map presets to sensible grouping (hourly/daily/weekly/monthly) based on available intervals\n- Keep backward compatibility: preserve existing URL/state params; keep legacy controls hidden\n- Add client support for new windows (12w, 12m) in time-bucket slicing\n- Show only relevant controls per tab (Trends: smoothing; Distribution: percent/group/exclude)\n- Streamline reset flow to a sane default (Last 7 days)

This commit is contained in:
ngxstat-bot 2025-08-19 00:09:49 -05:00
commit 95e54359d7

View file

@ -21,6 +21,7 @@
</div>
<div id="controls" class="field is-grouped is-align-items-center mb-4" style="position: sticky; top: 0; background: white; z-index: 2; padding: 0.5rem 0;">
<!-- Hidden native interval control kept for compatibility and availability probing -->
<div id="interval-control" class="control has-icons-left is-hidden">
<div class="select is-small">
<select id="interval-select">
@ -42,17 +43,20 @@
</div>
<span class="icon is-small is-left"><img src="icons/server.svg" alt="Domain"></span>
</div>
<div id="window-control" class="control has-icons-left is-hidden">
<!-- Unified Time control: selects both range and sensible grouping -->
<div id="time-control" class="control has-icons-left is-hidden">
<div class="select is-small">
<select id="window-select">
<option value="1h">Last 1h</option>
<option value="24h">Last 24h</option>
<option value="7d" selected>Last 7d</option>
<option value="30d">Last 30d</option>
<option value="all">All</option>
<select id="time-select">
<option value="1h">Last hour</option>
<option value="24h">Last 24 hours</option>
<option value="7d" selected>Last 7 days</option>
<option value="30d">Last 30 days</option>
<option value="12w">Last 12 weeks</option>
<option value="12m">Last 12 months</option>
<option value="all">All time</option>
</select>
</div>
<span class="icon is-small is-left"><img src="icons/pulse.svg" alt="Window"></span>
<span class="icon is-small is-left"><img src="icons/clock.svg" alt="Time"></span>
</div>
<div id="smooth-control" class="control is-hidden">
<label class="checkbox is-small">
@ -130,7 +134,8 @@
const domainSelect = document.getElementById('domain-select');
const intervalControl = document.getElementById('interval-control');
const domainControl = document.getElementById('domain-control');
const windowControl = document.getElementById('window-control');
const timeControl = document.getElementById('time-control');
const timeSelect = document.getElementById('time-select');
const modePercentControl = document.getElementById('mode-percent-control');
const modeGroupControl = document.getElementById('mode-group-control');
const excludeUncachedControl = document.getElementById('exclude-uncached-control');
@ -163,7 +168,25 @@
const elapsedElem = document.getElementById('stat-elapsed');
// Extra controls
// Legacy window select kept for internal state only (not shown)
const windowSelect = document.getElementById('window-select');
// If legacy window select is not present in DOM, create a hidden one for code paths
// that still reference it.
(function ensureHiddenWindowSelect(){
if (!windowSelect) {
const hidden = document.createElement('select');
hidden.id = 'window-select';
hidden.classList.add('is-hidden');
// Supported values used by code
['1h','24h','7d','30d','12w','12m','all'].forEach(v => {
const o = document.createElement('option');
o.value = v; o.textContent = v;
hidden.appendChild(o);
});
document.body.appendChild(hidden);
}
})();
const percentToggle = document.getElementById('percent-toggle');
const groupToggle = document.getElementById('group-toggle');
const excludeUncachedToggle = document.getElementById('exclude-uncached-toggle');
@ -172,7 +195,7 @@
let currentInterval = intervalSelect.value;
let currentDomain = domainSelect.value;
let currentTab = 'recent';
let currentWindow = windowSelect.value; // 1h, 24h, 7d, 30d, all
let currentWindow = windowSelect ? windowSelect.value : '7d'; // 1h, 24h, 7d, 30d, 12w, 12m, all
let modePercent = false;
let modeGroup = true;
let excludeUncached = true;
@ -241,10 +264,46 @@
case '24h': return interval === 'hourly' ? 24 : 'all';
case '7d': return interval === 'daily' ? 7 : 'all';
case '30d': return interval === 'daily' ? 30 : 'all';
case '12w': return interval === 'weekly' ? 12 : 'all';
case '12m': return interval === 'monthly' ? 12 : 'all';
default: return 'all';
}
}
function availableIntervals() {
try {
return Array.from(intervalSelect ? intervalSelect.options : []).map(o => o.value);
} catch { return []; }
}
function pickIntervalForWindow(win) {
const avail = availableIntervals();
const pref = (list) => list.find(x => avail.includes(x));
switch (win) {
case '1h':
case '24h':
return pref(['hourly','daily','weekly','monthly']) || (avail[0] || 'daily');
case '7d':
case '30d':
return pref(['daily','weekly','monthly','hourly']) || (avail[0] || 'daily');
case '12w':
return pref(['weekly','daily','monthly']) || (avail[0] || 'weekly');
case '12m':
return pref(['monthly','weekly','daily']) || (avail[0] || 'monthly');
default:
// all time: favor coarser buckets if available
return pref(['monthly','weekly','daily','hourly']) || (avail[0] || 'weekly');
}
}
function applyTimePreset(win) {
currentWindow = win;
currentInterval = pickIntervalForWindow(win);
if (intervalSelect) intervalSelect.value = currentInterval;
const winSel = document.getElementById('window-select');
if (winSel) winSel.value = currentWindow;
}
function initReport(token, rep, base) {
fetch(base + '/' + rep.json, { signal: token.controller.signal })
.then(r => r.json())
@ -517,11 +576,12 @@
Object.entries(sections).forEach(([key, section]) => {
section.classList.toggle('is-hidden', key !== name);
});
const showInterval = name !== 'recent' && name !== 'analysis';
const showDomain = showInterval;
intervalControl.classList.toggle('is-hidden', !showInterval);
const showTime = name !== 'recent' && name !== 'analysis';
const showDomain = showTime;
// Always keep legacy interval control hidden; use unified time control
intervalControl.classList.add('is-hidden');
domainControl.classList.toggle('is-hidden', !showDomain);
windowControl.classList.toggle('is-hidden', !showInterval);
timeControl.classList.toggle('is-hidden', !showTime);
// Only show percent/group/exclude toggles on Distribution tab,
// and smoothing only on Trends tab
modePercentControl.classList.toggle('is-hidden', name !== 'distribution');
@ -539,13 +599,15 @@
}
}
intervalSelect.addEventListener('change', () => {
currentInterval = intervalSelect.value;
abortLoad(currentLoad);
Object.values(containers).forEach(reset);
updateURL();
loadReports();
});
if (intervalSelect) {
intervalSelect.addEventListener('change', () => {
currentInterval = intervalSelect.value;
abortLoad(currentLoad);
Object.values(containers).forEach(reset);
updateURL();
loadReports();
});
}
domainSelect.addEventListener('change', () => {
currentDomain = domainSelect.value;
@ -555,12 +617,14 @@
loadReports();
});
windowSelect.addEventListener('change', () => {
currentWindow = windowSelect.value;
abortLoad(currentLoad);
updateURL();
loadReports();
});
if (timeSelect) {
timeSelect.addEventListener('change', () => {
applyTimePreset(timeSelect.value);
abortLoad(currentLoad);
updateURL();
loadReports();
});
}
percentToggle.addEventListener('change', () => {
modePercent = percentToggle.checked;
@ -602,9 +666,10 @@
} catch {}
// Reset to hard defaults
currentTab = 'recent';
currentInterval = intervalSelect.value = intervalSelect.options[0]?.value || currentInterval;
currentInterval = intervalSelect ? (intervalSelect.value = intervalSelect.options[0]?.value || currentInterval) : currentInterval;
currentDomain = domainSelect.value = '';
currentWindow = windowSelect.value = '7d';
applyTimePreset('7d');
if (timeSelect) timeSelect.value = '7d';
modePercent = percentToggle.checked = false;
modeGroup = groupToggle.checked = true;
excludeUncached = excludeUncachedToggle.checked = true;
@ -616,9 +681,15 @@
loadSavedState();
applyURLParams();
// Sync controls
intervalSelect.value = currentInterval;
if (intervalSelect) intervalSelect.value = currentInterval;
domainSelect.value = currentDomain;
windowSelect.value = currentWindow;
// Sync unified time select based on state
if (timeSelect) {
const known = new Set(['1h','24h','7d','30d','12w','12m','all']);
const pick = known.has(currentWindow) ? currentWindow : 'all';
timeSelect.value = pick;
applyTimePreset(pick);
}
percentToggle.checked = modePercent;
groupToggle.checked = modeGroup;
excludeUncachedToggle.checked = excludeUncached;