157b3e782017ad059b88a62fa75a349874188d4d
chmalee
  Fri Feb 14 12:23:00 2025 -0800
Calculate hubSpace directory size in UI as size of all child files, refs #31058

diff --git src/hg/js/hgMyData.js src/hg/js/hgMyData.js
index 726990fa523..8a4da8837fa 100644
--- src/hg/js/hgMyData.js
+++ src/hg/js/hgMyData.js
@@ -111,31 +111,32 @@
         if (thisQuota + hubCreate.uiState.userQuota > hubCreate.uiState.maxQuota) {
             uppy.info(`Error: this file batch exceeds your quota. Please delete some files to make space or email genome-www@soe.ucsc.edu if you feel you need more space.`);
             doUpload = false;
         }
         return doUpload;
     },
 });
 
 var hubCreate = (function() {
     let uiState = { // our object for keeping track of the current UI and what to do
         userUrl: "", // the web accesible path where the uploads are stored for this user
         hubNameDefault: "",
         isLoggedIn: "",
         maxQuota: 0,
         userQuota: 0,
-        userFiles: {},
+        userFiles: {}, // same as uiData.userFiles on page load
+        filesHash: {}, // for each file, userFiles.fullPath is the key, and then the userFiles.fileList data as the value, with an extra key for the child fullPaths if the file is a directory
     };
 
     function getTusdEndpoint() {
         // this variable is set by hgHubConnect and comes from hg.conf value
         return tusdEndpoint;
     }
 
     let extensionMap = {
         "bigBed": [".bb", ".bigbed"],
         "bam": [".bam"],
         "vcf": [".vcf"],
         "vcfTabix": [".vcf.gz", "vcf.bgz"],
         "bigWig": [".bw", ".bigwig"],
         "hic": [".hic"],
         "cram": [".cram"],
@@ -451,32 +452,78 @@
             // match the background color of the normal rows:
             rowNode.style.backgroundColor = "#f9f9f9";
             let thead = document.querySelector(".dt-scroll-headInner > table:nth-child(1) > thead:nth-child(1)");
             if (thead.childNodes.length === 1) {
                 thead.appendChild(rowClone);
             } else {
                 thead.replaceChild(rowClone, thead.lastChild);
             }
             // remove the row
             row.remove();
             // now do a regular order
             table.order([{name: "uploadTime", dir: "desc"}]);
         }
     }
 
+    function parseFileListIntoHash(fileList) {
+        // Hash the uiState fileList by the fullPath, and also store the children
+        // for each directory
+        // first go through and copy all of the data and make the empty
+        // children array for each directory
+        fileList.forEach(function(fileData) {
+            uiState.filesHash[fileData.fullPath] = fileData;
+            if (fileData.fileType === "dir") {
+                uiState.filesHash[fileData.fullPath].children = [];
+            }
+        });
+        // use a second pass to go through and set the children
+        // since we may not have encountered them yet in the above loop
+        fileList.forEach(function(fileData) {
+            if (fileData.fileType !== "dir" || fileData.parentDir !== "") {
+                // compute the key from the fullPath:
+                let parts = fileData.fullPath.split("/");
+                let keyName = parts.slice(0,-1).join("/");
+                if (keyName in uiState.filesHash) {
+                    uiState.filesHash[keyName].children.push(fileData);
+                }
+            }
+        });
+    }
+
+    function getChildRows(dirFullPath, childRowArray) {
+        // Recursively return all of the child rows for a given path
+        let childRows = uiState.filesHash[dirFullPath].children;
+        childRows.forEach(function(rowData) {
+            if (rowData.fileType !== "dir") {
+                childRowArray.push(rowData);
+            } else {
+                childRowArray.concat(getChildRows(rowData.fullPath, childRowArray));
+            }
+        });
+    }
+
     function dataTablePrintSize(data, type, row, meta) {
+        if (row.fileType !== "dir") {
             return prettyFileSize(data);
+        } else {
+            let childRows = [];
+            getChildRows(row.fullPath, childRows);
+            let sum = childRows.reduce( (accumulator, currentValue) => {
+                return accumulator + currentValue.fileSize;
+            }, 0);
+            return prettyFileSize(sum);
+        }
     }
 
     function dataTablePrintGenome(data, type, row, meta) {
         if (data.startsWith("hub_"))
             return data.split("_").slice(2).join("_");
         return data;
     }
 
     function dataTablePrintAction(rowData) {
         /* Return a node for rendering the actions column */
         if (rowData.fileType === "dir") {
             let folderIcon = document.createElement("i");
             folderIcon.style.display = "inline-block";
             folderIcon.style.backgroundImage = "url(\"../images/folderC.png\")";
             folderIcon.style.backgroundPosition = "left center";
@@ -526,55 +573,55 @@
     }
 
     function addFileToHub(rowData) {
         // a file has been uploaded and a hub has been created, present a modal
         // to choose which hub to associate this track to
         // backend wise: move the file into the hub directory
         //               update the hubSpace row with the hub name
         // frontend wise: move the file row into a 'child' of the hub row
         console.log(`sending addToHub req for ${rowData.fileName} to `);
         cart.setCgi("hgHubConnect");
         cart.send({addToHub: {hubName: "", dataFile: ""}});
         cart.flush();
     }
 
 
-    // hash of file paths to their objects, starts as uiState.userFiles
-    let filesHash = {};
     function addNewUploadedHubToTable(hub) {
         // hub is a list of objects representing the file just uploaded, the associated
         // hub.txt, and directory. Make a new row for each in the filesTable, except for
         // maybe the hub directory row and hub.txt which we may have already seen before
         let table = $("#filesTable").DataTable();
         let justUploaded = {}; // hash of contents of hub but keyed by fullPath
         let hubDirData = {}; // the data for the parentDir of the uploaded file
         for (let obj of hub) {
             if (!obj.parentDir) {
                 hubDirData = obj;
             }
             let rowObj;
-            if (!(obj.fullPath in filesHash)) {
+            if (!(obj.fullPath in uiState.filesHash)) {
                 justUploaded[obj.fullPath] = obj;
                 rowObj = table.row.add(obj);
-                filesHash[obj.fullPath] = obj;
                 uiState.fileList.push(obj);
+                // NOTE: we don't add the obj to the filesHash until after we're done
+                // so we don't need to reparse all files each time we add one
             }
         }
 
         // show all the new rows we just added, note the double draw, we need
         // to have the new rows rendered to do the order because the order
         // will copy the actual DOM node
+        parseFileListIntoHash(uiState.fileList);
         dataTableShowDir(table, hubDirData.fileName, hubDirData.fullPath);
         table.draw();
         dataTableCustomOrder(table, hubDirData);
         table.draw();
     }
 
     function doRowSelect(ev, table, indexes) {
         let row = table.row(indexes);
         let rowTr = row.node();
         if (rowTr) {
             let rowCheckbox = rowTr.childNodes[0].firstChild;
             if (rowTr.classList.contains("parentRow")) {
                 // we need to manually check the children
                 table.rows((idx,rowData) => rowData.fullPath.startsWith(row.data().fullPath) && rowData.parentDir === row.data().fileName).every(function(rowIdx, tableLoop, rowLoop) {
                     if (ev.type === "select") {
@@ -634,33 +681,33 @@
                 orderable: false, targets: 0,
                 render: DataTable.render.select(),
             },
             {
                 orderable: false, targets: 1,
                 data: "action", title: "",
                 render: function(data, type, row) {
                     if (type === "display") {
                         return dataTablePrintAction(row);
                     }
                     return '';
                 }
             },
             {
                 targets: 3,
-                render: function(data, type, row) {
+                render: function(data, type, row, meta) {
                     if (type === "display") {
-                         return dataTablePrintSize(data);
+                         return dataTablePrintSize(data, type, row, meta);
                     }
                     return data;
                 }
             },
             {
                 targets: 5,
                 render: function(data, type, row) {
                     if (type === "display") {
                         return dataTablePrintGenome(data);
                     }
                     return data;
                 }
             },
             {
                 // The upload time column
@@ -742,45 +789,43 @@
             document.getElementById("rootBreadcrumb").addEventListener("click", function(e) {
                 dataTableShowTopLevel(table);
                 dataTableCustomOrder(table);
                 dataTableEmptyBreadcrumb(table);
                 table.draw();
             });
         } else {
             table.buttons(".uploadButton").disable();
         }
         table.on("select", function(e, dt, type, indexes) {
             doRowSelect(e, dt, indexes);
         });
         table.on("deselect", function(e, dt, type, indexes) {
             doRowSelect(e, dt, indexes);
         });
-        _.each(d, function(f) {
-            filesHash[f.fullPath] = f;
-        });
         return table;
     }
 
     function init() {
         cart.setCgi('hgMyData');
         cart.debug(debugCartJson);
         // TODO: write functions for
         //     creating default trackDbs
         //     editing trackDbs
         // get the state from the history stack if it exists
         if (typeof uiData !== 'undefined' && typeof uiState.userFiles !== 'undefined') {
             _.assign(uiState, uiData.userFiles);
+            parseFileListIntoHash(uiState.fileList);
         }
         // first add the top level directories/files
         let table = showExistingFiles(uiState.fileList);
         // TODO: add event handlers for editing defaults, grouping into hub
         $("#newTrackHubDialog").dialog({
             modal: true,
             autoOpen: false,
             title: "Create new track hub",
             closeOnEscape: true,
             minWidth: 400,
             minHeight: 120
         });
 
         // create a custom uppy plugin to batch change the type and db fields
         class BatchChangePlugin extends Uppy.BasePlugin {
@@ -1048,30 +1093,30 @@
                 "fileName": "hub.txt",
                 "fileSize": 0,
                 "fileType": "hub.txt",
                 "genome": metadata.genome,
                 "parentDir": metadata.parentDir,
                 "fullPath": metadata.parentDir + "/hub.txt",
             };
             parentDirObj = {
                 "uploadTime": now.toLocaleString(),
                 "lastModified": d.toLocaleString(),
                 "fileName": metadata.parentDir,
                 "fileSize": 0,
                 "fileType": "dir",
                 "genome": metadata.genome,
                 "parentDir": "",
-                "fullPath": metadata.parentDir + "/",
+                "fullPath": metadata.parentDir,
             };
             // package the three objects together as one "hub" and display it
             let hub = [parentDirObj, hubTxtObj, newReqObj];
             addNewUploadedHubToTable(hub);
         });
         uppy.on('complete', (result) => {
             history.replaceState(uiState, "", document.location.href);
             console.log("replace history with uiState");
         });
     }
     return { init: init,
              uiState: uiState,
            };
 }());