6f41c6a196697c175a2cf5abcbd247ba24a2fe8e
jcasper
  Mon Jun 15 09:22:37 2026 -0700
Loading spinner for faceted composites (the page must first do the ajax metadata
fetch, then process the result with dataTables). refs #36320

diff --git src/hg/js/facetedComposite.js src/hg/js/facetedComposite.js
index 170169d36b6..4bf78703583 100644
--- src/hg/js/facetedComposite.js
+++ src/hg/js/facetedComposite.js
@@ -21,30 +21,43 @@
         }
         const fetchUrl = `/cgi-bin/hgTrackUi?${fetchBody}`;
         const req = (fetchUrl.length > 2048 || embeddedData.udcTimeout) ?
             fetch("/cgi-bin/hgTrackUi", {
                 method: "POST",
                 headers: { "Content-Type": "application/x-www-form-urlencoded" },
                 body: fetchBody,
             })
             : fetch(fetchUrl, {
                 method: "GET",
                 headers: { "Content-Type": "application/x-www-form-urlencoded" },
             });
         return req.then(r => r.ok ? r.json() : null).catch(() => null);
     };
 
+    const showLoading = () => {  // spinner shown during fetch + table build
+        if (document.getElementById("faceted-loading")) return;
+        const el = document.createElement("div");
+        el.id = "faceted-loading";
+        el.innerHTML =
+            `<div class="faceted-spinner"></div><div>Loading metadata…</div>`;
+        document.getElementById("metadata-placeholder").appendChild(el);
+    };
+    const hideLoading = () => {
+        const el = document.getElementById("faceted-loading");
+        if (el) el.remove();
+    };
+
     const toTitleStyle = str =>
             str.replace(/_+/g, " ");
 
     const escapeRegex = str => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
 
     // For primaryKey values that use the 'id|label' form, return just the id.
     // The label is for display only; the cart and rowToIdx need the bare id.
     const primaryKeyId = v => {
         if (v == null) return v;
         const s = String(v);
         const bar = s.indexOf("|");
         return bar >= 0 ? s.slice(0, bar) : s;
     };
 
     const embeddedData = (() => {
@@ -765,30 +778,31 @@
                 } else {
                     uriForUpdate.append(`${mdid}.dt_now`, "");
                 }
             }
             // No ${mdid}.dt* variables indicates that the composite doesn't use data types
 
             updateVisibilities(uriForUpdate, submitBtnEvent);
         });
     }  // end initSubmit
 
     function initAll(dataForTable) {
         initDataTypeSelector();
         const table = initTable(dataForTable);
         initFilters(table, dataForTable);
         initSubmit(table);
+        hideLoading();  // table is built and drawn; remove the spinner
     }
 
     function loadDataAndInit() {  // load data and call init functions
         const { mdid, primaryKey, metadataUrl, colorSettingsUrl, track } = embeddedData;
 
         const paramsFromUrl = new URLSearchParams(window.location.search);
         const hgsid = paramsFromUrl.get("hgsid");
         let fetchBody = `fileUrl=${metadataUrl}&track=${track}`;
         if (hgsid !== null) {
             fetchBody = fetchBody + `&hgsid=${hgsid}`;
         }
 
         // fetch file dynamically
         const fetchUrl = "/cgi-bin/hgTrackUi?" + fetchBody;
         const req = (fetchUrl.length > 2048 || embeddedData.udcTimeout) ?
@@ -828,32 +842,34 @@
                         row[primaryKey] != null && String(row[primaryKey]).includes(","));
                     if (badPk)
                         throw new Error(
                             `primaryKey column '${primaryKey}' contains a comma in value ` +
                             `'${badPk[primaryKey]}'; commas are not allowed in primaryKey values`);
                     const rowToIdx = Object.fromEntries(
                         metadata.map((row, i) => [primaryKeyId(row[primaryKey]), i])
                     );
                     colorMap = isValidColorMap(colorMap) ? colorMap : null;
                     const freshData = { metadata, rowToIdx, colNames, colorMap };
 
                     initAll(freshData);
                 });
             })
             .catch(err => {
+                hideLoading();  // stop the spinner before showing the error
                 const table = document.getElementById("theMetaDataTable");
                 if (table) {
                     table.innerHTML =
                         `<tr><td style="padding:20px;color:#a00;">` +
                         `Error loading metadata: ${err.message}</td></tr>`;
                 }
             });
     }  // end loadDataAndInit
 
     document.addEventListener("keydown", e => {  // block accidental submit
         if (e.key === "Enter") { e.preventDefault(); e.stopPropagation(); }
     }, true);
 
     generateHTML();
+    showLoading();  // show spinner immediately, before the metadata fetch
     loadDataAndInit();
 
 });