1c52aaa92828b6ecc18dd5800fe6650b91daf696
max
Fri Sep 18 07:22:10 2026 -0700
hgSession: the new Sessions page also lists the sessions saved on our other servers
#Preview2 week - bugs introduced now will need a build patch to fix
Every geo mirror node keeps its own namedSessionDb, so a session saved on
genome-euro is invisible on genome.ucsc.edu and people do not find it again.
The table now holds the sessions from every node, with a new Server column
saying where each one lives, and the session name links to the server that
holds it.
Two new hgSession actions, both answered in main() before the cart is built,
so that a request from another node leaves no cart, userDb or sessionDb row
behind:
hgS_doSessionListJson returns this server's sessions for the user named by
the login cookie, as JSON. This is what the other
nodes call.
hgS_doMirrorSessions asks every other gbNode for that list and returns
what came back. It sends all of the requests before
reading any of the answers, so the wait is the
slowest node rather than the sum of them.
The fetch goes through our own server rather than from the browser to the
other nodes, which keeps cross-origin requests out of it: apache handles the
CORS headers differently on every node, and the two mirrors are administered
by Bielefeld and RIKEN, while CGI code reaches them with the release. The
user's login cookies, and only those, are passed on (wikiLinkLoginCookieHeader),
which works because the nodes share a cookie salt.
The page never waits for any of this: the table is drawn from this server's
own sessions and the rows from the other servers are merged in as they
arrive, keeping the current sort, search and page. A node that does not
answer, times out, or runs a release without the endpoint is named in a line
under the toolbar instead, and costs nothing else.
Sessions from another server are shown but not managed here: no Share, Edit,
Overwrite or Delete, no bulk-select checkbox, and the Update now shortcut
keeps ignoring them. Table rows are keyed by a new uid rather than by the
session name, since the same name can now be in the table twice, once per
server.
geoMirrorThisNode() and geoMirrorOtherNodes() read the node list from the
hgcentral gbNode table, so nothing new has to be configured: browser.node
already says which node a server is.
diff --git src/hg/js/hgSession.js src/hg/js/hgSession.js
index 15160be5b58..fae84c8c90f 100644
--- src/hg/js/hgSession.js
+++ src/hg/js/hgSession.js
@@ -160,164 +160,175 @@
function sessAlert(title, bodyHtml, onOk) {
sessModalOpen(
'
' + sessEnc(title) + '
' +
'
' + bodyHtml + '
' +
'
' +
'
');
$('#sessAlertOk').on('click', function() {
sessModalClose();
if (onOk) { onOk(); }
});
document.getElementById('sessAlertOk').focus();
}
// ---- session lookup / row helpers ---------------------------------------
-function sessByEnc(enc) {
+function sessByKey(key) {
+ // Rows are keyed by uid, not by session name: the table also holds sessions merged in from our
+ // other servers, and the same name can exist on more than one of them.
var list = sessData.sessions;
for (var i = 0; i < list.length; i++) {
- if (list[i].encName === enc) { return list[i]; }
+ if (list[i].uid === key) { return list[i]; }
}
return null;
}
-function sessRowByEnc(enc) {
- // Return the DataTables row API for the session with this encName, or null.
+function sessRowByKey(key) {
+ // Return the DataTables row API for the session with this uid, or null.
var found = null;
sessDt.rows().every(function() {
- if (this.data().encName === enc) { found = this; }
+ if (this.data().uid === key) { found = this; }
});
return found;
}
function sessApplyTooltips() {
// Runs after every table draw, because DataTables renders rows on demand (page two, a re-sort)
// and the one-time conversion utils.js does at page load never sees those. Two jobs:
// - give the plain title attributes in the new rows the same styled mouseovers as the rest of
// the page, skipping whatever has already been converted;
// - hand each info bubble its session description. The description is the user's own text and
// the tooltip is inserted with innerHTML, so it is passed already escaped: markup in a
// description then reads as the characters that were typed instead of being parsed as HTML.
if (typeof titleTagToMouseover !== 'function' || typeof addMouseover !== 'function') { return; }
var $table = $('#sessionAppTable');
$table.find('[title]').each(function() {
if (this.title && this.getAttribute('mouseoverText') === null) { titleTagToMouseover(this); }
});
- $table.find('span.sessInfo[data-enc]').each(function() {
- var row = sessByEnc(this.getAttribute('data-enc'));
+ $table.find('span.sessInfo[data-key]').each(function() {
+ var row = sessByKey(this.getAttribute('data-key'));
if (row && row.description) { addMouseover(this, sessEnc(row.description)); }
});
}
// ---- table cell rendering ------------------------------------------------
// Inline icons (Font Awesome solid paths, embedded as SVG so they do not depend on the site's Font
// Awesome version). fill:currentColor picks up the button's text/danger color.
var SESS_TRASH_SVG = '';
// Font Awesome "floppy-disk" (save) regular/outline path - reads more clearly as a save icon.
var SESS_SAVE_SVG = '';
// Font Awesome "lock" solid path - marks a private (not-shared) session, since sharing is the default.
var SESS_LOCK_SVG = '';
function sessActionsHtml(row) {
// Order: Share, Edit, then the icon-only Overwrite (floppy) and Delete (trash).
- var e = sessEnc(row.encName);
- return '';
+ }
+ var e = sessEnc(row.uid);
+ return '' +
- '' +
- '' +
- '';
}
function sessNameCellHtml(row) {
var html = '' + sessEnc(row.name) + '';
if (row.description) {
// No title attribute here: the description is the user's own text and the tooltip machinery
// in utils.js inserts its text with innerHTML, so a title would have markup in a description
// parsed as HTML. sessApplyTooltips() attaches it, escaped, after the row is drawn.
- html += ' ⓘ';
+ html += ' ⓘ';
}
// Sessions are shared by default; mark only the exceptions: a lock for private, a badge for the
// public gallery. A plain shared-by-link session gets no marker.
if (row.shared === 0) {
html += ' ' +
SESS_LOCK_SVG + '';
} else if (row.shared >= 2) {
html += ' Public';
}
return html;
}
// ---- Overwrite -----------------------------------------------------------
function sessDoOverwrite(row) {
sessAjax(sessActParams(SESS_ACT.overwrite, row), function(resp) {
row.useCount = resp.useCount;
if (resp.created) { row.created = resp.created; }
if (resp.db) { row.db = resp.db; }
- var r = sessRowByEnc(row.encName);
+ var r = sessRowByKey(row.uid);
if (r) { r.data(row).draw(false); }
sessModalClose();
sessMsg('Overwrote “' + row.name + '” with your current browser view.', 'ok');
});
}
function sessOpenOverwrite(row) {
sessConfirm({
title: 'Overwrite session',
bodyHtml: 'Replace the saved session ' + sessEnc(row.name) + ' with the view you are ' +
'looking at now? The name and description stay the same; the previously saved view is lost.',
okLabel: 'Overwrite',
onOk: function() { sessDoOverwrite(row); }
});
}
// ---- Delete --------------------------------------------------------------
function sessOpenDelete(row) {
sessConfirm({
title: 'Delete session',
bodyHtml: 'Delete the session ' + sessEnc(row.name) + '? This cannot be undone. ' +
'Any shared links to it will stop working.',
okLabel: 'Delete',
okClass: 'danger',
onOk: function() {
sessAjax(sessActParams(SESS_ACT.del, row), function() {
- var r = sessRowByEnc(row.encName);
+ var r = sessRowByKey(row.uid);
if (r) { r.remove().draw(false); }
var list = sessData.sessions;
for (var i = 0; i < list.length; i++) {
- if (list[i].encName === row.encName) { list.splice(i, 1); break; }
+ if (list[i].uid === row.uid) { list.splice(i, 1); break; }
}
sessModalClose();
sessMsg('Deleted “' + row.name + '”.', 'ok');
});
}
});
}
// ---- Edit (rename + description) ----------------------------------------
function sessOpenEdit(row) {
var html = '