Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ jobs:
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-drag-manager-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1567-corner-clears-drag-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1306-collisions-terminology-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-205-analytics-subtab-deeplinks-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1374-route-map-a11y-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-channels-list-render-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-channels-selection-flow-e2e.js 2>&1 | tee -a e2e-output.txt
Expand Down
92 changes: 77 additions & 15 deletions public/analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,11 +223,11 @@
window: twElNow && twElNow.value ? twElNow.value : ''
};
// Drop any subview-specific keys that don't belong to the active tab
// so switching tabs gives a clean URL. (rf-health uses 'range', 'observer', 'from', 'to')
if (_currentTab !== 'rf-health') {
var cleared = ['range', 'observer', 'from', 'to'];
for (var i = 0; i < cleared.length; i++) updates[cleared[i]] = '';
}
// so switching tabs gives a clean URL.
Object.keys(TAB_URL_PARAMS).forEach(function (t) {
if (t === _currentTab) return;
TAB_URL_PARAMS[t].forEach(function (k) { updates[k] = ''; });
});
var newHash = URLState.updateHashParams(updates, location.hash);
if (newHash !== location.hash) history.replaceState(null, '', newHash);
}
Expand Down Expand Up @@ -340,6 +340,68 @@
return q ? path + (path.indexOf('?') < 0 ? '?' : '&') + q : path;
}

// #205 — a tab's inner view state (Scopes sub-tab and window, Wardriving
// window) lives in the hash next to ?tab=, with sessionStorage as the
// fallback for a plain visit of the tab. ?window= is the global time
// picker above the tab bar (other values, and it drives the shared
// loads), so each tab window gets a key of its own.
var SCOPES_SUBTAB = { param: 'sub', storageKey: 'scopes_subtab', allowed: ['overview', 'hopdepth', 'regions', 'hygiene'], dflt: 'overview' };
var SCOPES_WINDOW = { param: 'swin', storageKey: 'scopes_window', allowed: ['1h', '24h', '7d'], dflt: '24h' };
var WARDRIVING_WINDOW = { param: 'wdwin', storageKey: 'wardriving_window', allowed: ['1h', '24h', '7d'], dflt: '24h' };

// The hash keys each tab owns; _updateAnalyticsUrl drops them when
// another tab is selected.
var TAB_URL_PARAMS = {
'rf-health': ['range', 'observer', 'from', 'to'],
scopes: [SCOPES_SUBTAB.param, SCOPES_WINDOW.param],
wardriving: [WARDRIVING_WINDOW.param],
};

// A value from the URL wins; an unknown one falls back to the default,
// not to the stored value. Values are only compared with ===, never put
// in a selector or markup (#193/#194). Without a URL value, the stored
// value is used while it is still a known one.
function resolveViewParam(urlValue, storedValue, allowed, dflt) {
if (urlValue != null) return allowed.indexOf(urlValue) >= 0 ? urlValue : dflt;
return storedValue != null && allowed.indexOf(storedValue) >= 0 ? storedValue : dflt;
}

function _sessionGet(key) {
try { return typeof sessionStorage !== 'undefined' ? sessionStorage.getItem(key) : null; } catch (e) { return null; }
}

// Stores the values and writes them to the hash in one go. A default is
// left out, so a tab in its default view keeps the URL it had before #205.
function _writeViewParams(specs, values) {
var updates = {};
specs.forEach(function (spec, i) {
try { if (typeof sessionStorage !== 'undefined') sessionStorage.setItem(spec.storageKey, values[i]); } catch (e) { /* storage blocked */ }
updates[spec.param] = values[i] === spec.dflt ? '' : values[i];
});
if (!window.URLState) return;
// replaceState can throw (Safari throttles it); the view has already
// changed by then, so a failed URL sync must not break the tab (#1914).
try {
var newHash = URLState.updateHashParams(updates, location.hash);
if (newHash !== location.hash) history.replaceState(null, '', newHash);
} catch (e) { /* URL sync is best effort */ }
}

function setViewParam(spec, value) { _writeViewParams([spec], [value]); }

// Read on render: resolve every value of the tab from the same hash first,
// then store them and write them back. Writing one value rebuilds the
// hash, which drops an empty key ("?sub=") the next read would still see.
function restoreViewParams(specs) {
var hash = typeof location !== 'undefined' ? String(location.hash || '') : '';
var params = new URLSearchParams(hash.split('?')[1] || '');
var values = specs.map(function (spec) {
return resolveViewParam(params.get(spec.param), _sessionGet(spec.storageKey), spec.allowed, spec.dflt);
});
_writeViewParams(specs, values);
return values;
}

async function loadAnalytics(startedAt) {
_cancelLoadRetry();
const gen = _loadGen;
Expand Down Expand Up @@ -3159,6 +3221,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
if (typeof window !== 'undefined') {
window._analyticsAssignTableIds = assignAnalyticsTableIds;
window._analyticsWithQuery = withQuery;
window._analyticsResolveViewParam = resolveViewParam;
window._analyticsDecorateChannels = decorateAnalyticsChannels;
window._analyticsSortChannels = sortChannels;
window._analyticsLoadChannelSort = loadChannelSort;
Expand Down Expand Up @@ -4955,8 +5018,10 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf

// ===================== SCOPES =====================
async function renderScopesTab(el) {
var winKey = 'scopes_window';
var selectedWindow = (typeof sessionStorage !== 'undefined' && sessionStorage.getItem(winKey)) || '24h';
// Both views are deep-linked: ?sub= and ?swin= (#205).
var scopesView = restoreViewParams([SCOPES_SUBTAB, SCOPES_WINDOW]);
var selectedSubtab = scopesView[0];
var selectedWindow = scopesView[1];

// #1852: the tab grew to stacked sections (windowed adoption stats,
// all-time region breakdowns, all-time node/repeater hygiene lists) —
Expand All @@ -4970,8 +5035,6 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
// windowed panel gets its own copy of the picker buttons rather than
// one shared control above the sub-tab bar — every button still
// drives the same selectedWindow/load(), see the click listener below.
var subtabKey = 'scopes_subtab';
var selectedSubtab = (typeof sessionStorage !== 'undefined' && sessionStorage.getItem(subtabKey)) || 'overview';

// Role/text/geo filter for the "Nodes Without a Default Scope" section
// below. Lives at this scope (not inside updateData) so it survives
Expand Down Expand Up @@ -5058,9 +5121,9 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
var btn = e.target.closest('[data-subtab]');
if (!btn) return;
selectedSubtab = btn.dataset.subtab;
if (typeof sessionStorage !== 'undefined') sessionStorage.setItem(subtabKey, selectedSubtab);
setViewParam(SCOPES_SUBTAB, selectedSubtab);
subtabsEl.querySelectorAll('[data-subtab]').forEach(function(b) { b.classList.toggle('active', b.dataset.subtab === selectedSubtab); });
['overview', 'hopdepth', 'regions', 'hygiene'].forEach(function(key) {
SCOPES_SUBTAB.allowed.forEach(function(key) {
var panel = document.getElementById('scopes-panel-' + key);
if (panel) panel.style.display = key === selectedSubtab ? '' : 'none';
});
Expand All @@ -5071,7 +5134,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
el.querySelectorAll('[data-win]').forEach(function(btn) {
btn.addEventListener('click', function() {
selectedWindow = btn.dataset.win;
if (typeof sessionStorage !== 'undefined') sessionStorage.setItem(winKey, selectedWindow);
setViewParam(SCOPES_WINDOW, selectedWindow);
el.querySelectorAll('[data-win]').forEach(function(b) { b.classList.toggle('active', b.dataset.win === selectedWindow); });
load(selectedWindow);
});
Expand Down Expand Up @@ -6376,8 +6439,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
// and which observer stations — fixed, known locations — actually
// heard the traffic (Coverage).
async function renderWardrivingTab(el) {
var winKey = 'wardriving_window';
var selectedWindow = (typeof sessionStorage !== 'undefined' && sessionStorage.getItem(winKey)) || '24h';
var selectedWindow = restoreViewParams([WARDRIVING_WINDOW])[0]; // ?wdwin= (#205)

function pct(n, total) {
if (!total) return '—';
Expand Down Expand Up @@ -6659,7 +6721,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
el.querySelectorAll('[data-wdwin]').forEach(function(btn) {
btn.addEventListener('click', function() {
selectedWindow = btn.dataset.wdwin;
if (typeof sessionStorage !== 'undefined') sessionStorage.setItem(winKey, selectedWindow);
setViewParam(WARDRIVING_WINDOW, selectedWindow);
load(selectedWindow);
});
});
Expand Down
1 change: 1 addition & 0 deletions test-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ run test-packet-filter-time.js
run test-confidence-indicator.js
run test-1659-analytics-warmup.js
run test-analytics-tab-state-and-query.js
run test-analytics-subtab-deeplinks-205.js
run test-channels-merge-1498-unit.js
run test-issue-1518-home-url.js
run test-live-region-filter.js
Expand Down
Loading
Loading