4f03efa12fa7a52cad6b78f24d295ff5d80405c0
chmalee
  Thu Apr 23 12:14:00 2026 -0700
Try to make it more obvious that clicking the 'view' button next to a track file in hubspace connects the whole hub. Add a banner above the table indicating this and with a link that connects the entire hub, refs Max/Baihe email

diff --git src/hg/js/hgMyData.js src/hg/js/hgMyData.js
index d7fae2f2bcc..143b8b3f6be 100644
--- src/hg/js/hgMyData.js
+++ src/hg/js/hgMyData.js
@@ -1011,30 +1011,56 @@
                 // UCSC db - hgTracks needs 'genome=' (resolves via the hub)
                 // rather than 'db=' (looks up a native assembly).
                 let dbParam = hubType === "assemblyHub" ? "genome" : "db";
                 let url = "../cgi-bin/hgTracks?hgsid=" + getHgsid() + "&" + dbParam + "=" + genome + "&hubUrl=" + encodeURIComponent(hubUrl) + "&" + trackHubFixName(fname) + "=pack";
                 window.location.assign(url);
                 return false;
             }
         }
     }
 
     function trackHubFixName(trackName) {
         // replace everything but alphanumeric and underscore with underscore
         return encodeURIComponent(trackName.replaceAll(fileNameFixRegex, "_"));
     }
 
+    function viewHubInGenomeBrowser(hubName) {
+        // connect the whole hub in hgTracks, without pack'ing any specific track
+        if (typeof uiState.userUrl === "undefined" || uiState.userUrl.length === 0) {
+            return;
+        }
+        let dirRow = uiState.filesHash[hubName];
+        if (!dirRow) return;
+        let hubUrl = uiState.userUrl + cgiEncode(hubTxtPathForHub(hubName));
+        let dbParam = dirRow.hubType === "assemblyHub" ? "genome" : "db";
+        let url = "../cgi-bin/hgTracks?hgsid=" + getHgsid() + "&" + dbParam + "=" + dirRow.genome + "&hubUrl=" + encodeURIComponent(hubUrl);
+        window.location.assign(url);
+    }
+
+    function showHubBanner(hubName) {
+        let banner = document.getElementById("hubBanner");
+        let nameSpan = document.getElementById("hubBannerName");
+        if (!banner || !nameSpan) return;
+        nameSpan.textContent = hubName;
+        banner.style.display = "";
+    }
+
+    function hideHubBanner() {
+        let banner = document.getElementById("hubBanner");
+        if (banner) banner.style.display = "none";
+    }
+
     // helper object so we don't need to use an AbortController to update
     // the data this function is using
     let selectedData = {};
     // track which items the user directly selected (vs children of selected directories)
     let directlySelected = {};
     function viewAllInGenomeBrowser(ev) {
         // redirect to hgTracks with these tracks/hubs open
         let data = selectedData;
         if (typeof uiState.userUrl !== "undefined" && uiState.userUrl.length > 0) {
             let url = "../cgi-bin/hgTracks?hgsid=" + getHgsid();
             let genome; // may be multiple genomes in list, just redirect to the first one
                         // TODO: this should probably raise an alert to click through
             let hubsAdded = {};
             _.forEach(data, (d) => {
                 if (!genome) {
@@ -1214,56 +1240,58 @@
         // clear any fixed searches so we can apply a new one
         let currSearches = table.search.fixed().toArray();
         currSearches.forEach((name) => table.search.fixed(name, null));
     }
 
     function dataTableShowTopLevel(table) {
         // show all the "root" files, which are files (probably mostly directories)
         // with no parentDir
         clearSearch(table);
         // deselect any selected rows like Finder et al when moving into/upto a directory
         table.rows({selected: true}).deselect();
         table.search.fixed("showRoot", function(searchStr, rowData, rowIx) {
             return !rowData.parentDir;
         });
         uiState.currentHub = "";
+        hideHubBanner();
     }
 
     function dataTableShowDir(table, dirName, dirFullPath) {
         // show the directory and all immediate children of the directory
         clearSearch(table);
         // deselect any selected rows like Finder et al when moving into/upto a directory
         table.rows({selected: true}).deselect();
         table.draw();
         // NOTE that the below does not actually render until the next table.draw() call
         table.search.fixed("oneHub", function(searchStr, rowData, rowIx) {
             // calculate the fullPath of this rows parentDir in case the dirName passed
             // to this function has the same name as a parentDir further up in the
             // listing. For example, consider a test/test/tmp.txt layout, where "test"
             // is the parentDir of tmp.txt and the test subdirectory
             let parentDirFull = rowData.fullPath.split("/").slice(0,-1).join("/");
             if (rowData.parentDir === dirName && parentDirFull === dirFullPath) {
                 return true;
             } else if (rowData.fullPath === dirFullPath) {
                 // also return the directory itself
                 return true;
             } else {
                 return false;
             }
         });
         uiState.currentHub = dirName;
         dataTableCreateBreadcrumb(table, dirName, dirFullPath);
+        showHubBanner(dirName);
     }
 
     // when we move into a new directory, we remove the row from the table
     // and add it's html into the header, keep the row object around so
     // we can add it back in later
     let oldRowData = null;
     function dataTableCustomOrder(table, dirData) {
         // figure out the order the rows of the table should be in
         // if dirData is null, sort on  uploadTime first
         // if dirData exists, that is the first row, followed by everything else
         // in uploadTime order
         if (!dirData) {
             // make sure the old row can show up again in the table
             let thead = document.querySelector(".dt-scroll-headInner > table:nth-child(1) > thead:nth-child(1)");
             if (thead.childNodes.length > 1) {
@@ -1715,30 +1743,36 @@
             }
             return container;
         });
         let table = new DataTable("#filesTable", tableInitOptions);
         if (uiState.isLoggedIn) {
             table.buttons(".uploadButton").enable();
             document.getElementById("rootBreadcrumb").addEventListener("click", function(e) {
                 dataTableShowTopLevel(table);
                 dataTableCustomOrder(table);
                 dataTableEmptyBreadcrumb(table);
                 table.draw();
             });
         } else {
             table.buttons(".uploadButton").disable();
         }
+        let hubBannerBtn = document.getElementById("hubBannerViewBtn");
+        if (hubBannerBtn) {
+            hubBannerBtn.addEventListener("click", function(e) {
+                viewHubInGenomeBrowser(uiState.currentHub);
+            });
+        }
         table.on("select", function(e, dt, type, indexes) {
             indexes.forEach(function(i) {
                 doRowSelect(e.type, dt, i);
             });
         });
         table.on("deselect", function(e, dt, type, indexes) {
             indexes.forEach(function(i) {
                 doRowSelect(e.type, dt, i);
             });
         });
         table.on("click", function(e) {
             let copyIcon = e.target.closest ? e.target.closest(".copyLinkIcon") : null;
             if (copyIcon) {
                 e.stopPropagation();
                 e.preventDefault();