eb076410a62ea6a94b71358424080af261ba2c25
jcasper
  Thu Mar 26 03:20:14 2026 -0700
Moved hgFetch into hgTrackUi; added etags, Cache-Control directives, and GET use instead of POST
when udcTimeout isn't set, all to support browser caching of remote files; bumped the DataTables version for facetedComposite
to facilitate making all the checkboxes have a consistent look; refs #36320

diff --git src/hg/js/facetedComposite.js src/hg/js/facetedComposite.js
index 4074198a754..932a5c954a0 100644
--- src/hg/js/facetedComposite.js
+++ src/hg/js/facetedComposite.js
@@ -1,46 +1,54 @@
 // SPDX-License-Identifier: MIT; (c) 2025 Andrew D Smith (author)
 /* jshint esversion: 11 */
 $(function() {
     /* ADS: Uncomment below to force confirm on unload/reload */
     // window.addEventListener("beforeunload", function (e) {
     //     e.preventDefault(); e.returnValue = ""; });
     const DEFAULT_MAX_CHECKBOXES = 20;  // ADS: without default, can get crazy
 
     // ADS: need "matching" versions for the plugins
-    const DATATABLES_URL = "../js/dataTables-1.13.6.min.js";
-    const DATATABLES_SELECT_URL = "../js/dataTables.select-1.7.0.min.js";
+    const DATATABLES_URL = "../js/dataTables-2.2.2.min.js";
+    const DATATABLES_SELECT_URL = "../js/dataTables.select-3.0.0.min.js";
     const CSS_URLS = [
-        "../style/dataTables-1.13.6.min.css",  // dataTables CSS
-        "../style/dataTables.select-1.7.0.min.css",  // dataTables Select CSS
+        "../style/dataTables-2.2.2.min.css",  // dataTables CSS
+        "../style/dataTables.select-3.0.0.min.css",  // dataTables Select CSS
         "../style/facetedComposite.css",  // Local metadata table CSS
     ];
 
     const isValidColorMap = obj =>  // check the whole thing and ignore if invalid
           typeof obj === "object" && obj !== null && !Array.isArray(obj) &&
           Object.values(obj).every(x =>
               typeof x === "object" && x !== null && !Array.isArray(x) &&
                   Object.values(x).every(value => typeof value === "string"));
 
-    const loadOptional = (url, hgsid, track) =>  // load if possible otherwise carry on
-          url ?
-          fetch("/cgi-bin/hgFetch", {
+    // fetch file dynamically
+    const loadOptional = (url, hgsid, track) =>  { // load if possible otherwise carry on
+        if (!url) return Promise.resolve(null);
+        const fetchUrl = `/cgi-bin/hgTrackUi?hgsid=${hgsid}&fileUrl=${url}&track=${track}`;
+        const req = (fetchUrl.length > 2048 || embeddedData.udcTimeout) ?
+            fetch("/cgi-bin/hgTrackUi", {
                 method: "POST",
                 headers: { "Content-Type": "application/x-www-form-urlencoded" },
                 body: `hgsid=${hgsid}&fileUrl=${url}&track=${track}`,
-          }).then(r => r.ok ? r.json() : null).catch(() => null)
-          : Promise.resolve(null);
+            })
+            : fetch(fetchUrl, {
+                method: "GET",
+                headers: { "Content-Type": "application/x-www-form-urlencoded" },
+            });
+        return req.then(r => r.ok ? r.json() : null).catch(() => null);
+    };
 
     const loadIfMissing = (condition, url, callback) =>  // for missing plugins
           condition ?
           document.head.appendChild(Object.assign(
               document.createElement("script"), { src: url, onload: callback }))
           : callback();
 
     const toTitleCase = str =>
           str.toLowerCase()
           .split(/[_\s-]+/)  // Split on underscore, space, or hyphen
           .map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
 
     const escapeRegex = str => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
 
     const embeddedData = (() => {
@@ -120,102 +128,106 @@
         // Capture initial data type state
         initialState.dataTypes = new Set(selectedDataTypes);
     }
 
     function initTable(allData) {
         const { metadata, rowToIdx, colNames } = allData;
 
         const ordinaryColumns = colNames.map(key => ({  // all but checkboxes
             data: key,
             title: toTitleCase(key.replace(/^_/, "")),
         }));
 
         const checkboxColumn = {
             data: null,
             orderable: false,
-            className: "select-checkbox",
             defaultContent: "",
             title: `
             <label title="Select all visible rows">
             <input type="checkbox" id="select-all"/></label>`,
             // no render function needed
         };
 
         const columns = [checkboxColumn, ...ordinaryColumns];
         const hasDataTypes = embeddedData.dataTypes &&
                              Object.keys(embeddedData.dataTypes).length > 0;
         const itemLabel = hasDataTypes ? "samples" : "tracks";
         const table = $("#theMetaDataTable").DataTable({
             data: metadata,
             deferRender: true,    // seems faster
             columns: columns,
+            columnDefs: [ { targets:0, render: DataTable.render.select() } ],
             responsive: true,
-            dom: "lrtip",         // omit 'f' (global search); per-column inputs suffice
-            // autoWidth: true,   // might help columns shrink to fit content
+            layout: {
+                topStart: 'pageLength',
+                topEnd: null,        // omit global search
+                bottomStart: 'info',
+                bottomEnd: 'paging'
+            },
             order: [[1, "asc"]],  // sort by the first data column, not checkbox
             pageLength: 50,       // show 50 rows per page by default
             lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]],
             language: { lengthMenu: `Show _MENU_ ${itemLabel}`, },
             select: { style: "multi", selector: "td:not(:has(a))" },
             initComplete: function() {  // Check appropriate boxes
                 const api = this.api();
                 embeddedData.dataElements.forEach(rowName => {
                     const rowIndex = rowToIdx[rowName];
                     if (rowIndex !== undefined) {
                         api.row(rowIndex).select();
                     }
                 });
                 // Capture initial data element state
                 initialState.dataElements = new Set(embeddedData.dataElements);
             },
             drawCallback: function() {  // Reset header "select all" checkbox
                 $("#select-all")
                     .prop("checked", false)
                     .prop("indeterminate", false);
             },
         });
 
         // Create "show only selected" toggle in the toolbar
         const lengthDiv = document.querySelector(
-            "#theMetaDataTable_wrapper .dataTables_length");
+            "#theMetaDataTable_wrapper .dt-length");
         const toggleWrapper = document.createElement("div");
         toggleWrapper.id = "selected-filter";
         const toggleLabel = document.createElement("label");
         toggleLabel.classList.add("toggle-switch");
         const toggleCheckbox = document.createElement("input");
         toggleCheckbox.type = "checkbox";
         toggleCheckbox.dataset.selectFilter = "true";
         toggleLabel.appendChild(toggleCheckbox);
         toggleLabel.appendChild(Object.assign(
             document.createElement("span"), {className: "toggle-slider"}));
         toggleWrapper.appendChild(toggleLabel);
         toggleWrapper.appendChild(Object.assign(
             document.createElement("span"), {textContent: "Show only selected rows"}));
         lengthDiv.appendChild(toggleWrapper);
 
         // Create active-filters chip bar (hidden when empty)
         const activeFiltersDiv = document.createElement("div");
         activeFiltersDiv.id = "active-filters";
         activeFiltersDiv.style.display = "none";
         const wrapper = document.getElementById("theMetaDataTable_wrapper");
         wrapper.insertBefore(activeFiltersDiv, lengthDiv.nextSibling);
 
         // define inputs for search functionality for each column in the table
         const row = document.querySelector("#theMetaDataTable thead").insertRow();
         columns.forEach((col) => {
             const cell = row.insertCell();
-            if (col.className === "select-checkbox") {
+            if (col.data === null) {
                 // left empty; toggle is now in the toolbar
             } else if (col.data && col.data.startsWith("_")) {
                 // no search box for hidden-facet columns
             } else {
                 const input = document.createElement("input");
                 input.type = "text";
                 input.placeholder = "Search...";
                 input.style.width = "100%";
                 cell.appendChild(input);
             }
         });
 
         // behaviors for the column-based search functionality
         $("#theMetaDataTable thead input[type='text']")
             .on("keyup change", function () {
@@ -518,36 +530,43 @@
         const table = initTable(dataForTable);
         initFilters(table, dataForTable);
         initSubmit(table);
     }
 
     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("/cgi-bin/hgFetch", {
+        // fetch file dynamically
+        let 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,
             })
-            .then(response => {
+            : fetch(fetchUrl, {
+                method: "GET",
+                headers: { "Content-Type": "application/x-www-form-urlencoded" },
+            });
+            req.then(response => {
                 if (!response.ok) {  // a 404 will look like plain text
                     throw new Error(`HTTP Status: ${response.status}`);
                 }
                 return response.text();
             })
             .then(tsvText => {  // metadata table is a TSV file to parse
                 loadOptional(colorSettingsUrl, hgsid, track).then(colorMap => {
                     const rows = tsvText.trim().split("\n");
                     const colNames = rows[0].split("\t");
                     const metadata = rows.slice(1).map(row => {
                         const values = row.split("\t");
                         const obj = {};
                         colNames.forEach((attrib, i) => { obj[attrib] = values[i]; });
                         return obj;
                     });