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:
parent
6de85b7cc5
commit
95e54359d7
1 changed files with 102 additions and 31 deletions
|
@ -21,6 +21,7 @@
|
||||||
</div>
|
</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;">
|
<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 id="interval-control" class="control has-icons-left is-hidden">
|
||||||
<div class="select is-small">
|
<div class="select is-small">
|
||||||
<select id="interval-select">
|
<select id="interval-select">
|
||||||
|
@ -42,17 +43,20 @@
|
||||||
</div>
|
</div>
|
||||||
<span class="icon is-small is-left"><img src="icons/server.svg" alt="Domain"></span>
|
<span class="icon is-small is-left"><img src="icons/server.svg" alt="Domain"></span>
|
||||||
</div>
|
</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">
|
<div class="select is-small">
|
||||||
<select id="window-select">
|
<select id="time-select">
|
||||||
<option value="1h">Last 1h</option>
|
<option value="1h">Last hour</option>
|
||||||
<option value="24h">Last 24h</option>
|
<option value="24h">Last 24 hours</option>
|
||||||
<option value="7d" selected>Last 7d</option>
|
<option value="7d" selected>Last 7 days</option>
|
||||||
<option value="30d">Last 30d</option>
|
<option value="30d">Last 30 days</option>
|
||||||
<option value="all">All</option>
|
<option value="12w">Last 12 weeks</option>
|
||||||
|
<option value="12m">Last 12 months</option>
|
||||||
|
<option value="all">All time</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
||||||
<div id="smooth-control" class="control is-hidden">
|
<div id="smooth-control" class="control is-hidden">
|
||||||
<label class="checkbox is-small">
|
<label class="checkbox is-small">
|
||||||
|
@ -130,7 +134,8 @@
|
||||||
const domainSelect = document.getElementById('domain-select');
|
const domainSelect = document.getElementById('domain-select');
|
||||||
const intervalControl = document.getElementById('interval-control');
|
const intervalControl = document.getElementById('interval-control');
|
||||||
const domainControl = document.getElementById('domain-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 modePercentControl = document.getElementById('mode-percent-control');
|
||||||
const modeGroupControl = document.getElementById('mode-group-control');
|
const modeGroupControl = document.getElementById('mode-group-control');
|
||||||
const excludeUncachedControl = document.getElementById('exclude-uncached-control');
|
const excludeUncachedControl = document.getElementById('exclude-uncached-control');
|
||||||
|
@ -163,7 +168,25 @@
|
||||||
const elapsedElem = document.getElementById('stat-elapsed');
|
const elapsedElem = document.getElementById('stat-elapsed');
|
||||||
|
|
||||||
// Extra controls
|
// Extra controls
|
||||||
|
// Legacy window select kept for internal state only (not shown)
|
||||||
const windowSelect = document.getElementById('window-select');
|
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 percentToggle = document.getElementById('percent-toggle');
|
||||||
const groupToggle = document.getElementById('group-toggle');
|
const groupToggle = document.getElementById('group-toggle');
|
||||||
const excludeUncachedToggle = document.getElementById('exclude-uncached-toggle');
|
const excludeUncachedToggle = document.getElementById('exclude-uncached-toggle');
|
||||||
|
@ -172,7 +195,7 @@
|
||||||
let currentInterval = intervalSelect.value;
|
let currentInterval = intervalSelect.value;
|
||||||
let currentDomain = domainSelect.value;
|
let currentDomain = domainSelect.value;
|
||||||
let currentTab = 'recent';
|
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 modePercent = false;
|
||||||
let modeGroup = true;
|
let modeGroup = true;
|
||||||
let excludeUncached = true;
|
let excludeUncached = true;
|
||||||
|
@ -241,10 +264,46 @@
|
||||||
case '24h': return interval === 'hourly' ? 24 : 'all';
|
case '24h': return interval === 'hourly' ? 24 : 'all';
|
||||||
case '7d': return interval === 'daily' ? 7 : 'all';
|
case '7d': return interval === 'daily' ? 7 : 'all';
|
||||||
case '30d': return interval === 'daily' ? 30 : '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';
|
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) {
|
function initReport(token, rep, base) {
|
||||||
fetch(base + '/' + rep.json, { signal: token.controller.signal })
|
fetch(base + '/' + rep.json, { signal: token.controller.signal })
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
|
@ -517,11 +576,12 @@
|
||||||
Object.entries(sections).forEach(([key, section]) => {
|
Object.entries(sections).forEach(([key, section]) => {
|
||||||
section.classList.toggle('is-hidden', key !== name);
|
section.classList.toggle('is-hidden', key !== name);
|
||||||
});
|
});
|
||||||
const showInterval = name !== 'recent' && name !== 'analysis';
|
const showTime = name !== 'recent' && name !== 'analysis';
|
||||||
const showDomain = showInterval;
|
const showDomain = showTime;
|
||||||
intervalControl.classList.toggle('is-hidden', !showInterval);
|
// Always keep legacy interval control hidden; use unified time control
|
||||||
|
intervalControl.classList.add('is-hidden');
|
||||||
domainControl.classList.toggle('is-hidden', !showDomain);
|
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,
|
// Only show percent/group/exclude toggles on Distribution tab,
|
||||||
// and smoothing only on Trends tab
|
// and smoothing only on Trends tab
|
||||||
modePercentControl.classList.toggle('is-hidden', name !== 'distribution');
|
modePercentControl.classList.toggle('is-hidden', name !== 'distribution');
|
||||||
|
@ -539,13 +599,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
intervalSelect.addEventListener('change', () => {
|
if (intervalSelect) {
|
||||||
currentInterval = intervalSelect.value;
|
intervalSelect.addEventListener('change', () => {
|
||||||
abortLoad(currentLoad);
|
currentInterval = intervalSelect.value;
|
||||||
Object.values(containers).forEach(reset);
|
abortLoad(currentLoad);
|
||||||
updateURL();
|
Object.values(containers).forEach(reset);
|
||||||
loadReports();
|
updateURL();
|
||||||
});
|
loadReports();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
domainSelect.addEventListener('change', () => {
|
domainSelect.addEventListener('change', () => {
|
||||||
currentDomain = domainSelect.value;
|
currentDomain = domainSelect.value;
|
||||||
|
@ -555,12 +617,14 @@
|
||||||
loadReports();
|
loadReports();
|
||||||
});
|
});
|
||||||
|
|
||||||
windowSelect.addEventListener('change', () => {
|
if (timeSelect) {
|
||||||
currentWindow = windowSelect.value;
|
timeSelect.addEventListener('change', () => {
|
||||||
abortLoad(currentLoad);
|
applyTimePreset(timeSelect.value);
|
||||||
updateURL();
|
abortLoad(currentLoad);
|
||||||
loadReports();
|
updateURL();
|
||||||
});
|
loadReports();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
percentToggle.addEventListener('change', () => {
|
percentToggle.addEventListener('change', () => {
|
||||||
modePercent = percentToggle.checked;
|
modePercent = percentToggle.checked;
|
||||||
|
@ -602,9 +666,10 @@
|
||||||
} catch {}
|
} catch {}
|
||||||
// Reset to hard defaults
|
// Reset to hard defaults
|
||||||
currentTab = 'recent';
|
currentTab = 'recent';
|
||||||
currentInterval = intervalSelect.value = intervalSelect.options[0]?.value || currentInterval;
|
currentInterval = intervalSelect ? (intervalSelect.value = intervalSelect.options[0]?.value || currentInterval) : currentInterval;
|
||||||
currentDomain = domainSelect.value = '';
|
currentDomain = domainSelect.value = '';
|
||||||
currentWindow = windowSelect.value = '7d';
|
applyTimePreset('7d');
|
||||||
|
if (timeSelect) timeSelect.value = '7d';
|
||||||
modePercent = percentToggle.checked = false;
|
modePercent = percentToggle.checked = false;
|
||||||
modeGroup = groupToggle.checked = true;
|
modeGroup = groupToggle.checked = true;
|
||||||
excludeUncached = excludeUncachedToggle.checked = true;
|
excludeUncached = excludeUncachedToggle.checked = true;
|
||||||
|
@ -616,9 +681,15 @@
|
||||||
loadSavedState();
|
loadSavedState();
|
||||||
applyURLParams();
|
applyURLParams();
|
||||||
// Sync controls
|
// Sync controls
|
||||||
intervalSelect.value = currentInterval;
|
if (intervalSelect) intervalSelect.value = currentInterval;
|
||||||
domainSelect.value = currentDomain;
|
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;
|
percentToggle.checked = modePercent;
|
||||||
groupToggle.checked = modeGroup;
|
groupToggle.checked = modeGroup;
|
||||||
excludeUncachedToggle.checked = excludeUncached;
|
excludeUncachedToggle.checked = excludeUncached;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue