0ae2a6a36d7b4c77e8e7d7df2a1049f4412e7099
max
  Fri Sep 18 07:03:44 2026 -0700
hgHubConnect: let a mirror hand out its own API keys, and stop swallowing links to another host

#Preview2 week - bugs introduced now will need a build patch to fix
Two of the three problems QA found on #38323.

The link in the mirror-only Hub Upload message did not go anywhere. The tab
handler in hgHubConnect.js catches every hgHubConnect link with a hash and turns
it into a tab switch, and 'Go to Hub Upload on genome.ucsc.edu' has a hash that
names a tab on the mirror too, so the click just reopened the tab the reader was
already on. It now only intercepts links to the page itself.

The API key section was decoupled from storeUserFiles, but only for display: the
Generate and Revoke buttons are cartJson requests, and both the javascript that
sends them and the code in main() that routes them were still inside the
storeUserFiles gate. A site with showHubApiKey on and hubSpace off therefore drew
two dead buttons. The key functions move out of hgMyData.js into a new
hubApiKey.js that the Hub Development tab includes on its own, main() routes a
cartJson request when either setting is on, and the hubSpace file commands stay
registered only when hubSpace is actually running.

The request also goes to this host's hgHubConnect now rather than to the login
host. Keys live in the central database of the server that issues them, so a key
made on genome-euro belongs in genome-euro's table.

refs #38323

diff --git src/hg/js/hgMyData.js src/hg/js/hgMyData.js
index 5a578a838a2..787629ec271 100644
--- src/hg/js/hgMyData.js
+++ src/hg/js/hgMyData.js
@@ -1,39 +1,41 @@
 /* jshint esversion: 8 */
 
 /* This file contains all the code needed to get the HubSpace UI functioning.
  * There are some helper functions that are sort of general and could probably
  * be added to utils.js or similar as well as 3 main pieces:
- * - the api key generation/revocation: functions to request new/revoke old apiKeys
- *       this code runs on the Hub Development tab of hgHubConnect
  * - uppyOptions and uppy constructor: Uppy is a 3rd party library for handling
  *       user uploads. The uppyOptions object and constructor are used to modify
  *       the default behavior, including what to do when a file has been added
  *       to the dashboard, verifying file name legality, etc. Code in these sections
  *       also modifies the default preact/react rendering of elements, so it looks
  *       a little different than normal kent javascript.
  * - BatchChangePlugin class: a custom class again used to extend the default Uppy
  *       interface. This time to put some inputs at the bottom of the dashboard
  *       that changes metadata for all the files a user has selected
  * - hubCreate: An IIFE that runs on document ready that sets up or controls the
  *       whole UI. The UI is a combindation of Uppy for the actual file selection
  *       and DataTables for showing the uploaded files. There are many helper functions
  *       within this block that also could probably be moved to a lib, but haven't as
  *       the code has evolved over time.
  *
  *   TODO: most of this code could probably be modularized successfully, or split up
  *   so it is easier to read.
+ *
+ * The API key generation and revocation used to be a fourth piece here. It is in
+ * hubApiKey.js now: the Hub Development tab offers those controls on sites that do not
+ * run hubSpace, and such a site never loads this file.
  */
 
 
 var debugCartJson = true;
 
 function prettyFileSize(num) {
     if (!num) {return "0B";}
     if (num < (1024 * 1024)) {
         return `${(num/1024).toFixed(1)}KB`;
     } else if (num < (1024 * 1024 * 1024)) {
         return `${((num/1024)/1024).toFixed(1)}MB`;
     } else {
         return `${(((num/1024)/1024)/1024).toFixed(1)}GB`;
     }
 }
@@ -170,87 +172,30 @@
     if (roots.length === 1) {
         input.value = roots[0];
     }
 }
 
 function refreshBatchSelects(uppyInstance) {
     // Rebuild the batch controls so the genome box shows what the files actually
     // carry. addBatchSelectsToDashboard only rebuilds when the batch changed shape,
     // so this is cheap to call after anything that restamps genome metadata
     let plugin = uppyInstance.getPlugin("BatchChangePlugin");
     if (plugin && uppyInstance.getFiles().length > 1) {
         plugin.addBatchSelectsToDashboard();
     }
 }
 
-function generateApiKey() {
-    let apiKeyInstr = document.getElementById("apiKeyInstructions");
-    let apiKeyDiv = document.getElementById("apiKey");
-
-    if (!document.getElementById("spinner")) {
-        let spinner = document.createElement("i");
-        spinner.id = "spinner";
-        spinner.classList.add("fa", "fa-spinner", "fa-spin");
-        document.getElementById("generateApiKey").after(spinner);
-    }
-
-    let handleSuccess = function(reqObj) {
-        apiKeyDiv.textContent = reqObj.apiKey;
-        apiKeyInstr.style.display = "block";
-        let revokeDiv= document.getElementById("revokeDiv");
-        revokeDiv.style.display = "block";
-        document.getElementById("spinner").remove();
-
-        // remove the word 'already' from the message if we have just re-generated a key
-        let refreshSpan = document.getElementById("removeOnGenerate");
-        if (refreshSpan) {
-            refreshSpan.style.display = "none";
-        }
-    };
-
-    let cartData = {generateApiKey: {}};
-    cart.setCgiAndUrl(fileListEndpoint);
-    cart.send(cartData, handleSuccess);
-    cart.flush();
-}
-
-function revokeApiKeys() {
-    let apiKeyInstr = document.getElementById("apiKeyInstructions");
-    let apiKeyDiv = document.getElementById("apiKey");
-
-    if (!document.getElementById("spinner")) {
-        let spinner = document.createElement("i");
-        spinner.id = "spinner";
-        spinner.classList.add("fa", "fa-spinner", "fa-spin");
-        document.getElementById("revokeApiKeys").after(spinner);
-    }
-
-    let handleSuccess = function(req) {
-        apiKeyInstr.style.display = "none";
-        document.getElementById("spinner").remove();
-        let generateDiv = document.getElementById("generateDiv");
-        generateDiv.style.display = "block";
-        let revokeDiv = document.getElementById("revokeDiv");
-        revokeDiv.style.display = "none";
-    };
-
-    let cartData = {revokeApiKey: {}};
-    cart.setCgiAndUrl(fileListEndpoint);
-    cart.send(cartData, handleSuccess);
-    cart.flush();
-}
-
 const fileNameRegex = /[0-9a-zA-Z._]+/g; // allowed characters in file names
 const fileNameFixRegex = /[^0-9a-zA-Z_]+/g; // '.' get replaced to underbars in trackHub.c. Also any files uploaded from hubtools that may have weird chars need to be escaped
 const parentDirSegmentRegex = /^[0-9a-zA-Z._]+$/; // allowed characters in each hub-path segment
 
 function normalizeParentDir(file) {
     // Strip surrounding whitespace off a file's parentDir, writing the trimmed value back
     // into the file metadata. A trailing space is invisible in the hub name field, so
     // rejecting it outright gives the user an error they cannot see the cause of. Must be
     // called before isValidParentDir so we validate what will actually be uploaded.
     let parentDir = (file.meta && file.meta.parentDir) || "";
     let trimmed = parentDir.trim();
     if (trimmed !== parentDir) {
         uppy.setFileMeta(file.id, {parentDir: trimmed});
         file.meta.parentDir = trimmed;
     }