7206132fbc62b1d3b563b67aa067ae8d6ab76e2b
chmalee
  Fri Jun 6 15:54:08 2025 -0700
Refactor hubSpace UI a little in order to make getting the initial filelist over async rather than with the CGI load, refs Max discussion

diff --git src/hg/js/hgMyData.js src/hg/js/hgMyData.js
index 70a4e3719ff..e0ab03de997 100644
--- src/hg/js/hgMyData.js
+++ src/hg/js/hgMyData.js
@@ -162,30 +162,141 @@
         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.setCgi("hgHubConnect");
     cart.send(cartData, handleSuccess);
     cart.flush();
 }
 
 const fileNameRegex = /[0-9a-zA-Z._\-+]+/g; // allowed characters in file names
 const parentDirRegex = /[0-9a-zA-Z._\-+]+/g; // allowed characters in hub names
+
+function getTusdEndpoint() {
+    // this variable is set by hgHubConnect and comes from hg.conf value
+    return tusdEndpoint;
+}
+
+let uppyOptions = {
+    trigger: ".uploadButton",
+    showProgressDetails: true,
+    note: "The UCSC Genome Browser is not a HIPAA compliant data store. Do not upload patient information or other sensitive data files here, as anyone with the URL can view them.",
+    meta: {"genome": null, "fileType": null},
+    restricted: {requiredMetaFields: ["genome"]},
+    closeModalOnClickOutside: true,
+    closeAfterFinish: true,
+    theme: 'auto',
+    metaFields: (file) => {
+        const fields = [{
+            id: 'name',
+            name: 'File name',
+            render: ({value, onChange, required, form}, h) => {
+                return h('input',
+                    {type: "text",
+                    value: value,
+                    class: "uppy-u-reset uppy-c-textInput uppy-Dashboard-FileCard-input",
+                    onChange: e => {
+                        onChange(e.target.value);
+                        file.meta.fileType = hubCreate.detectFileType(e.target.value);
+                        file.meta.name = e.target.value;
+                    },
+                    required: required,
+                    form: form,
+                    }
+                );
+            },
+        },
+        {
+            id: 'genome',
+            name: 'Genome',
+            render: ({value, onChange}, h) => {
+                // keep these as a variable so we can init the autocompleteCat
+                // code only after the elements have actually been rendered
+                // there are multiple rendering passes and only eventually
+                // do the elements actually make it into the DOM
+                let ret = h('div', {
+                        class: "uppy-Dashboard-FileCard-label",
+                        style: "display: inline-block; width: 78%"
+                        },
+                    // first child of div
+                    "Select from popular assemblies:",
+                    // second div child
+                    h('select', {
+                        id: `${file.meta.name}DbSelect`,
+                        style: "margin-left: 5px",
+                        onChange: e => {
+                            onChange(e.target.value);
+                            file.meta.genome = e.target.value;
+                            file.meta.genomeLabel = e.target.selectedOptions[0].label;
+                        }
+                        },
+                        hubCreate.makeGenomeSelectOptions(file.meta.genome, file.meta.genomeLabel).map( (genomeObj) => {
+                            return h('option', {
+                                value: genomeObj.value,
+                                label: genomeObj.label,
+                                selected: file.meta.genome !== null ? genomeObj.value === file.meta.genome : genomeObj.value === hubCreate.defaultDb()
+                            });
+                        })
+                    ),
+                    h('p', {
+                        class: "uppy-Dashboard-FileCard-label",
+                        style: "display: block; width: 78%",
+                        }, "or search for your genome:"),
+                    // third div child
+                    h('input', {
+                        id: `${file.meta.name}DbInput`,
+                        type: 'text',
+                        class: "uppy-u-reset uppy-c-textInput uppy-Dashboard-FileCard-input",
+                    }),
+                    h('input', {
+                        id: `${file.meta.name}DbSearchButton`,
+                        type: 'button',
+                        value: 'search',
+                        style: "margin-left: 5px",
+                    })
+                );
+            let selectToChange = document.getElementById(`${file.meta.name}DbSelect`);
+            if (selectToChange) {
+                let justInitted = initAutocompleteForInput(`${file.meta.name}DbInput`, selectToChange);
+                if (justInitted) {
+                    // only do this once per file
+                    document.getElementById(`${file.meta.name}DbSearchButton`)
+                            .addEventListener("click", (e) => {
+                                let inp = document.getElementById(`${file.meta.name}DbInput`).value;
+                                let selector = `[id='${file.meta.name}DbInput']`;
+                                $(selector).autocompleteCat("search", inp);
+                            });
+                }
+            }
+            return ret;
+            }
+        },
+        {
+            id: 'parentDir',
+            name: 'Hub Name',
+        }];
+        return fields;
+    },
+    doneButtonHandler: function() {
+        uppy.clear();
+    },
+};
+
 // make our Uppy instance:
 const uppy = new Uppy.Uppy({
     debug: true,
     allowMultipleUploadBatches: false,
     onBeforeUpload: (files) => {
         // set all the fileTypes and genomes from their selects
         let doUpload = true;
         let thisQuota = 0;
         for (let [key, file] of Object.entries(files)) {
             let fileNameMatch = file.meta.name.match(fileNameRegex);
             let parentDirMatch = file.meta.parentDir.match(parentDirRegex);
             if (!fileNameMatch || fileNameMatch[0] !== file.meta.name) {
                 uppy.info(`Error: File name has special characters, please rename file: ${file.meta.name} to only include alpha-numeric characters, period, dash, underscore or plus.`, 'error', 5000);
                 doUpload = false;
                 continue;
@@ -207,46 +318,265 @@
             uppy.setFileMeta(file.id, {
                 fileName: file.meta.name,
                 fileSize: file.size,
                 lastModified: file.data.lastModified,
             });
             thisQuota += file.size;
         }
         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;
     },
 });
 
+// create a custom uppy plugin to batch change the type and db fields
+class BatchChangePlugin extends Uppy.BasePlugin {
+    constructor(uppy, opts) {
+        super(uppy, opts);
+        this.id = "BatchChangePlugin";
+        this.type = "progressindicator";
+        this.opts = opts;
+    }
+
+    createOptsForSelect(select, opts) {
+        opts.forEach( (opt) => {
+            let option = document.createElement("option");
+            option.value = opt.value;
+            option.label = opt.label;
+            option.id = opt.id;
+            option.selected = typeof opt.selected !== 'undefined' ? opt.selected : false;
+            select.appendChild(option);
+        });
+    }
+
+    addSelectsForFile(file) {
+        /* create two selects for the file object, to include the db and type */
+        const id = "uppy_" + file.id;
+        let fileDiv = document.getElementById(id);
+        // this might not exist yet depending on where we are in the render cycle
+        if (fileDiv) {
+            let dbSelectId = "db_select_" + file.id;
+            if (!document.getElementById(dbSelectId)) {
+                let dbSelect = document.createElement("select");
+                dbSelect.id = dbSelectId;
+                let dbOpts = hubCreate.makeGenomeSelectOptions();
+                this.createOptsForSelect(dbSelect, dbOpts);
+                fileDiv.appendChild(dbSelect);
+            }
+        }
+    }
+
+    removeBatchSelectsFromDashboard() {
+        let batchSelectDiv = document.getElementById("batch-selector-div");
+        if (batchSelectDiv) {
+            batchSelectDiv.remove();
+        }
+    }
+
+    addBatchSelectsToDashboard() {
+        if (!document.getElementById("batch-selector-div")) {
+            let batchSelectDiv = document.createElement("div");
+            batchSelectDiv.id = "batch-selector-div";
+            batchSelectDiv.style.display = "grid";
+            batchSelectDiv.style.width = "80%";
+            // the grid syntax is 2 columns, 3 rows
+            batchSelectDiv.style.gridTemplateColumns = "max-content minmax(0, 200px) max-content 1fr min-content";
+            batchSelectDiv.style.gridTemplateRows = "repest(3, auto)";
+            batchSelectDiv.style.margin = "10px auto"; // centers this div
+            batchSelectDiv.style.fontSize = "14px";
+            batchSelectDiv.style.gap = "8px";
+            if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
+                batchSelectDiv.style.color = "#eaeaea";
+            }
+
+            // first just explanatory text:
+            let batchSelectText = document.createElement("div");
+            batchSelectText.textContent = "Change options for all files:";
+            // syntax here is rowStart / columnStart / rowEnd / columnEnd
+            batchSelectText.style.gridArea = "1 / 1 / 1 / 2";
+
+            // the batch change db select
+            let batchDbSelect = document.createElement("select");
+            this.createOptsForSelect(batchDbSelect, hubCreate.makeGenomeSelectOptions());
+            batchDbSelect.id = "batchDbSelect";
+            batchDbSelect.style.gridArea = "2 / 2 / 2 / 2";
+            batchDbSelect.style.margin = "2px";
+            let batchDbLabel = document.createElement("label");
+            batchDbLabel.textContent = "Genome";
+            batchDbLabel.for = "batchDbSelect";
+            batchDbLabel.style.gridArea = "2 / 1 / 2 / 1";
+
+            // the search bar for db selection
+            let batchDbSearchBarLabel= document.createElement("label");
+            batchDbSearchBarLabel.textContent = "or search for your genome:";
+            batchDbSearchBarLabel.style.gridArea = "2 / 3 /2 / 3";
+            batchDbSearchBarLabel.style.margin = "auto";
+
+            let batchDbGenomeSearchBar = document.createElement("input");
+            batchDbGenomeSearchBar.classList.add("uppy-u-reset", "uppy-c-textInput");
+            batchDbGenomeSearchBar.type = "text";
+            batchDbGenomeSearchBar.id = "batchDbSearchBar";
+            batchDbGenomeSearchBar.style.gridArea = "2 / 4 / 2 / 4";
+            let batchDbGenomeSearchButton = document.createElement("input");
+            batchDbGenomeSearchButton.type = "button";
+            batchDbGenomeSearchButton.value = "search";
+            batchDbGenomeSearchButton.id = "batchDbSearchBarButton";
+            batchDbGenomeSearchButton.style.gridArea = "2 / 5 / 2 / 5";
+
+            // the batch change hub name
+            let batchParentDirLabel = document.createElement("label");
+            batchParentDirLabel.textContent = "Hub Name";
+            batchParentDirLabel.for = "batchParentDir";
+            batchParentDirLabel.style.gridArea = "3 / 1 / 3 / 1";
+
+            let batchParentDirInput = document.createElement("input");
+            batchParentDirInput.id = "batchParentDir";
+            batchParentDirInput.value = hubCreate.uiState.hubNameDefault;
+            batchParentDirInput.style.gridArea = "3 / 2 / 3 / 2";
+            batchParentDirInput.style.margin= "1px 1px auto";
+            batchParentDirInput.classList.add("uppy-u-reset", "uppy-c-textInput");
+
+            // add event handlers to change metadata, use an arrow function
+            // because otherwise 'this' keyword will be the element instead of
+            // our class
+            batchDbSelect.addEventListener("change", (ev) => {
+                let files = this.uppy.getFiles();
+                let val = ev.target.value;
+                for (let [key, file] of Object.entries(files)) {
+                    this.uppy.setFileMeta(file.id, {genome: val});
+                    this.uppy.setFileMeta(file.id, {genomeLabel: ev.target.selectedOptions[0].label});
+                }
+            });
+            batchParentDirInput.addEventListener("change", (ev) => {
+                let files = this.uppy.getFiles();
+                let val = ev.target.value;
+                for (let [key, file] of Object.entries(files)) {
+                    this.uppy.setFileMeta(file.id, {parentDir: val});
+                }
+            });
+
+
+            batchSelectDiv.appendChild(batchSelectText);
+            batchSelectDiv.appendChild(batchDbLabel);
+            batchSelectDiv.appendChild(batchDbSelect);
+            batchSelectDiv.appendChild(batchDbSearchBarLabel);
+            batchSelectDiv.appendChild(batchDbGenomeSearchBar);
+            batchSelectDiv.appendChild(batchDbGenomeSearchButton);
+            batchSelectDiv.appendChild(batchParentDirLabel);
+            batchSelectDiv.appendChild(batchParentDirInput);
+
+            // append the batch changes to the bottom of the file list, for some reason
+            // I can't append to the actual Dashboard-files, it must be getting emptied
+            // and re-rendered or something
+            let uppyFilesDiv = document.querySelector(".uppy-Dashboard-progressindicators");
+            if (uppyFilesDiv) {
+                uppyFilesDiv.insertBefore(batchSelectDiv, uppyFilesDiv.firstChild);
+            }
+
+            // everything has to exist already for autocompleteCat to initialize
+            let justInitted = initAutocompleteForInput(batchDbGenomeSearchBar.id, batchDbSelect);
+            if (justInitted) {
+                // only do this once per batch setup
+                batchDbGenomeSearchButton.addEventListener("click", (e) => {
+                    let inp = document.getElementById(batchDbSearchBar.id).value;
+                    let selector = "[id='"+batchDbGenomeSearchBar.id+"']";
+                    $(selector).autocompleteCat("search", inp);
+                });
+            }
+        }
+    }
+
+    install() {
+        this.uppy.on("file-added", (file) => {
+            // add default meta data for genome and fileType
+            console.log("file-added");
+            this.uppy.setFileMeta(file.id, {"genome": hubCreate.defaultDb(), "fileType": hubCreate.detectFileType(file.name), "parentDir": hubCreate.uiState.hubNameDefault});
+            if (this.uppy.getFiles().length > 1) {
+                this.addBatchSelectsToDashboard();
+            } else {
+                // only open the file editor when there is one file
+                const dash = uppy.getPlugin("Dashboard");
+                dash.toggleFileCard(true, file.id);
+            }
+        });
+        this.uppy.on("file-removed", (file) => {
+            // remove the batch change selects if now <2 files present
+            if (this.uppy.getFiles().length < 2) {
+                this.removeBatchSelectsFromDashboard();
+            }
+        });
+
+        this.uppy.on("dashboard:modal-open", () => {
+            // check if there were already files chosen from before:
+            if (this.uppy.getFiles().length > 2) {
+                this.addBatchSelectsToDashboard();
+            }
+            if (this.uppy.getFiles().length < 2) {
+                this.removeBatchSelectsFromDashboard();
+            }
+        });
+        this.uppy.on("dashboard:modal-closed", () => {
+            if (this.uppy.getFiles().length < 2) {
+                this.removeBatchSelectsFromDashboard();
+            }
+            let allFiles = this.uppy.getFiles();
+            let completeFiles = this.uppy.getFiles().filter((f) => f.progress.uploadComplete === true);
+            if (allFiles.length === completeFiles.length) {
+                this.uppy.clear();
+            }
+        });
+        this.uppy.on("dashboard:file-edit-start", (file) => {
+            autocompletes[`${file.name}DbInput`] = false;
+        });
+
+        this.uppy.on("dashboard:file-edit-complete", (file) => {
+            // check the filename and hubname metadata and warn the user
+            // to edit them if they are wrong. unfortunately I cannot
+            // figure out how to force the file card to re-toggle
+            // and jump back into the editor from here
+            if (file) {
+                let fileNameMatch = file.meta.name.match(fileNameRegex);
+                let parentDirMatch = file.meta.parentDir.match(parentDirRegex);
+                const dash = uppy.getPlugin("Dashboard");
+                if (!fileNameMatch || fileNameMatch[0] !== file.meta.name) {
+                    uppy.info(`Error: File name has special characters, please rename file: '${file.meta.name}' to only include alpha-numeric characters, period, dash, underscore or plus.`, 'error', 5000);
+                }
+                if (!parentDirMatch || parentDirMatch[0] !== file.meta.parentDir) {
+                    uppy.info(`Error: Hub name has special characters, please rename hub: '${file.meta.parentDir}' to only include alpha-numeric characters, period, dash, underscore, or plus.`, 'error', 5000);
+                }
+            }
+        });
+    }
+    uninstall() {
+        // not really used because we aren't ever uninstalling the uppy instance
+        this.uppy.off("file-added");
+    }
+}
+
 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: {}, // 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"],
         "bigBarChart": [".bigbarchart"],
         "bigGenePred": [".bgp", ".biggenepred"],
         "bigMaf": [".bigmaf"],
         "bigInteract": [".biginteract"],
         "bigPsl": [".bigpsl"],
         "bigChain": [".bigchain"],
         "bamIndex": [".bam.bai", ".bai"],
@@ -911,484 +1241,143 @@
             }
         ],
         columns: [
             {data: "", },
             {data: "", },
             {data: "fileName", title: "File name"},
             {data: "fileSize", title: "File size"},
             {data: "fileType", title: "File type"},
             {data: "genome", title: "Genome"},
             {data: "parentDir", title: "Hubs"},
             {data: "lastModified", title: "File Last Modified"},
             {data: "uploadTime", title: "Upload Time", name: "uploadTime"},
             {data: "fullPath", title: "fullPath", name: "fullPath"},
         ],
         drawCallback: function(settings) {
-            console.log("table draw");
-        },
-        rowCallback: function(row, data, displayNum, displayIndex, dataIndex) {
-            // row is a tr element, data is the td values
-            // a row can represent one of three things:
-            // a 'folder', with no parents, but with children
-            // a folder with parents and children (can only come from hubtools
-            // a 'file' with no children, but with parentDir
-            // we assign the appropriate classes which are used later to
-            // collapse/expand and select rows for viewing or deletion
-            if (!data.parentDir) {
-                row.className = "topLevelRow";
-            } else {
-                row.className = "childRow";
-            }
-            if (data.fileType === "dir") {
-                row.className += " parentRow";
-            }
-            indentActionButton(row, data);
-        },
-        initComplete: function(settings, json) {
-            console.log("data loaded, only showing directories");
-            let table = new $.fn.dataTable.Api(settings);
-            dataTableShowTopLevel(table);
-            dataTableCustomOrder(table);
-            table.draw();
-        }
-    };
-
-    function showExistingFiles(d) {
-        // Make the DataTable for each file
-        // make buttons have the same style as other buttons
-        $.fn.dataTable.Buttons.defaults.dom.button.className = 'button';
-        tableInitOptions.data = d;
-        if (uiState.isLoggedIn) {
-            tableInitOptions.language = {emptyTable: "Uploaded files will appear here. Click \"Upload\" to get started"};
-        } else {
-            tableInitOptions.language = {emptyTable: "You are not logged in, please navigate to \"My Data\" > \"My Sessions\" and log in or create an account to begin uploading files"};
-        }
-        DataTable.feature.register('quota', function(settings, opts) {
-            let options = Object.assign({option1: false, option2: false}, opts);
-            let container = document.createElement("div");
-            if (uiState.isLoggedIn) {
-                container.textContent = `Using ${prettyFileSize(uiState.userQuota)} of ${prettyFileSize(uiState.maxQuota)}`;
-            }
-            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();
-        }
-        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) {
-            if (e.target.className !== "dt-select-checkbox") {
-                e.stopPropagation();
-                // we've clicked somewhere not on the checkbox itself, we need to:
-                // 1. open the directory if the clicked row is a directory
-                // 2. select the file if the clicked row is a regular file
-                let row = table.row(e.target);
-                let data = row.data();
-                if (data.children && data.children.length > 0) {
-                    dataTableShowDir(table, data.fileName, data.fullPath);
-                    dataTableCustomOrder(table, {"fullPath": data.fullPath});
-                    table.draw();
-                } else {
-                    if (row.selected()) {
-                        row.deselect();
-                        doRowSelect("deselect", table, row.index());
-                    } else {
-                        row.select();
-                        doRowSelect("select", table, row.index());
-                    }
-                }
-            }
-        });
-        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);
-            if (uiState.fileList) {
-                parseFileListIntoHash(uiState.fileList);
-            }
-        }
-        // first add the top level directories/files
-        let table = showExistingFiles(uiState.fileList);
-        table.columns.adjust().draw();
-        // 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 {
-            constructor(uppy, opts) {
-                super(uppy, opts);
-                this.id = "BatchChangePlugin";
-                this.type = "progressindicator";
-                this.opts = opts;
-            }
-
-            createOptsForSelect(select, opts) {
-                opts.forEach( (opt) => {
-                    let option = document.createElement("option");
-                    option.value = opt.value;
-                    option.label = opt.label;
-                    option.id = opt.id;
-                    option.selected = typeof opt.selected !== 'undefined' ? opt.selected : false;
-                    select.appendChild(option);
-                });
-            }
-
-            addSelectsForFile(file) {
-                /* create two selects for the file object, to include the db and type */
-                const id = "uppy_" + file.id;
-                let fileDiv = document.getElementById(id);
-                // this might not exist yet depending on where we are in the render cycle
-                if (fileDiv) {
-                    let dbSelectId = "db_select_" + file.id;
-                    if (!document.getElementById(dbSelectId)) {
-                        let dbSelect = document.createElement("select");
-                        dbSelect.id = dbSelectId;
-                        let dbOpts = makeGenomeSelectOptions();
-                        this.createOptsForSelect(dbSelect, dbOpts);
-                        fileDiv.appendChild(dbSelect);
-                    }
-                }
-            }
-
-            removeBatchSelectsFromDashboard() {
-                let batchSelectDiv = document.getElementById("batch-selector-div");
-                if (batchSelectDiv) {
-                    batchSelectDiv.remove();
-                }
-            }
-
-            addBatchSelectsToDashboard() {
-                if (!document.getElementById("batch-selector-div")) {
-                    let batchSelectDiv = document.createElement("div");
-                    batchSelectDiv.id = "batch-selector-div";
-                    batchSelectDiv.style.display = "grid";
-                    batchSelectDiv.style.width = "80%";
-                    // the grid syntax is 2 columns, 3 rows
-                    batchSelectDiv.style.gridTemplateColumns = "max-content minmax(0, 200px) max-content 1fr min-content";
-                    batchSelectDiv.style.gridTemplateRows = "repest(3, auto)";
-                    batchSelectDiv.style.margin = "10px auto"; // centers this div
-                    batchSelectDiv.style.fontSize = "14px";
-                    batchSelectDiv.style.gap = "8px";
-                    if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
-                        batchSelectDiv.style.color = "#eaeaea";
-                    }
-
-                    // first just explanatory text:
-                    let batchSelectText = document.createElement("div");
-                    batchSelectText.textContent = "Change options for all files:";
-                    // syntax here is rowStart / columnStart / rowEnd / columnEnd
-                    batchSelectText.style.gridArea = "1 / 1 / 1 / 2";
-
-                    // the batch change db select
-                    let batchDbSelect = document.createElement("select");
-                    this.createOptsForSelect(batchDbSelect, makeGenomeSelectOptions());
-                    batchDbSelect.id = "batchDbSelect";
-                    batchDbSelect.style.gridArea = "2 / 2 / 2 / 2";
-                    batchDbSelect.style.margin = "2px";
-                    let batchDbLabel = document.createElement("label");
-                    batchDbLabel.textContent = "Genome";
-                    batchDbLabel.for = "batchDbSelect";
-                    batchDbLabel.style.gridArea = "2 / 1 / 2 / 1";
-
-                    // the search bar for db selection
-                    let batchDbSearchBarLabel= document.createElement("label");
-                    batchDbSearchBarLabel.textContent = "or search for your genome:";
-                    batchDbSearchBarLabel.style.gridArea = "2 / 3 /2 / 3";
-                    batchDbSearchBarLabel.style.margin = "auto";
-
-                    let batchDbGenomeSearchBar = document.createElement("input");
-                    batchDbGenomeSearchBar.classList.add("uppy-u-reset", "uppy-c-textInput");
-                    batchDbGenomeSearchBar.type = "text";
-                    batchDbGenomeSearchBar.id = "batchDbSearchBar";
-                    batchDbGenomeSearchBar.style.gridArea = "2 / 4 / 2 / 4";
-                    let batchDbGenomeSearchButton = document.createElement("input");
-                    batchDbGenomeSearchButton.type = "button";
-                    batchDbGenomeSearchButton.value = "search";
-                    batchDbGenomeSearchButton.id = "batchDbSearchBarButton";
-                    batchDbGenomeSearchButton.style.gridArea = "2 / 5 / 2 / 5";
-
-                    // the batch change hub name
-                    let batchParentDirLabel = document.createElement("label");
-                    batchParentDirLabel.textContent = "Hub Name";
-                    batchParentDirLabel.for = "batchParentDir";
-                    batchParentDirLabel.style.gridArea = "3 / 1 / 3 / 1";
-
-                    let batchParentDirInput = document.createElement("input");
-                    batchParentDirInput.id = "batchParentDir";
-                    batchParentDirInput.value = uiState.hubNameDefault;
-                    batchParentDirInput.style.gridArea = "3 / 2 / 3 / 2";
-                    batchParentDirInput.style.margin= "1px 1px auto";
-                    batchParentDirInput.classList.add("uppy-u-reset", "uppy-c-textInput");
-
-                    // add event handlers to change metadata, use an arrow function
-                    // because otherwise 'this' keyword will be the element instead of
-                    // our class
-                    batchDbSelect.addEventListener("change", (ev) => {
-                        let files = this.uppy.getFiles();
-                        let val = ev.target.value;
-                        for (let [key, file] of Object.entries(files)) {
-                            this.uppy.setFileMeta(file.id, {genome: val});
-                            this.uppy.setFileMeta(file.id, {genomeLabel: ev.target.selectedOptions[0].label});
-                        }
-                    });
-                    batchParentDirInput.addEventListener("change", (ev) => {
-                        let files = this.uppy.getFiles();
-                        let val = ev.target.value;
-                        for (let [key, file] of Object.entries(files)) {
-                            this.uppy.setFileMeta(file.id, {parentDir: val});
-                        }
-                    });
-
-
-                    batchSelectDiv.appendChild(batchSelectText);
-                    batchSelectDiv.appendChild(batchDbLabel);
-                    batchSelectDiv.appendChild(batchDbSelect);
-                    batchSelectDiv.appendChild(batchDbSearchBarLabel);
-                    batchSelectDiv.appendChild(batchDbGenomeSearchBar);
-                    batchSelectDiv.appendChild(batchDbGenomeSearchButton);
-                    batchSelectDiv.appendChild(batchParentDirLabel);
-                    batchSelectDiv.appendChild(batchParentDirInput);
-
-                    // append the batch changes to the bottom of the file list, for some reason
-                    // I can't append to the actual Dashboard-files, it must be getting emptied
-                    // and re-rendered or something
-                    let uppyFilesDiv = document.querySelector(".uppy-Dashboard-progressindicators");
-                    if (uppyFilesDiv) {
-                        uppyFilesDiv.insertBefore(batchSelectDiv, uppyFilesDiv.firstChild);
-                    }
-
-                    // everything has to exist already for autocompleteCat to initialize
-                    let justInitted = initAutocompleteForInput(batchDbGenomeSearchBar.id, batchDbSelect);
-                    if (justInitted) {
-                        // only do this once per batch setup
-                        batchDbGenomeSearchButton.addEventListener("click", (e) => {
-                            let inp = document.getElementById(batchDbSearchBar.id).value;
-                            let selector = "[id='"+batchDbGenomeSearchBar.id+"']";
-                            $(selector).autocompleteCat("search", inp);
-                        });
+            console.log("table draw");
+        },
+        rowCallback: function(row, data, displayNum, displayIndex, dataIndex) {
+            // row is a tr element, data is the td values
+            // a row can represent one of three things:
+            // a 'folder', with no parents, but with children
+            // a folder with parents and children (can only come from hubtools
+            // a 'file' with no children, but with parentDir
+            // we assign the appropriate classes which are used later to
+            // collapse/expand and select rows for viewing or deletion
+            if (!data.parentDir) {
+                row.className = "topLevelRow";
+            } else {
+                row.className = "childRow";
             }
+            if (data.fileType === "dir") {
+                row.className += " parentRow";
             }
+            indentActionButton(row, data);
+        },
+        initComplete: function(settings, json) {
+            console.log("data loaded, only showing directories");
+            let table = new $.fn.dataTable.Api(settings);
+            dataTableShowTopLevel(table);
+            dataTableCustomOrder(table);
+            table.draw();
         }
+    };
 
-            install() {
-                this.uppy.on("file-added", (file) => {
-                    // add default meta data for genome and fileType
-                    console.log("file-added");
-                    this.uppy.setFileMeta(file.id, {"genome": defaultDb(), "fileType": detectFileType(file.name), "parentDir": uiState.hubNameDefault});
-                    if (this.uppy.getFiles().length > 1) {
-                        this.addBatchSelectsToDashboard();
+    function showExistingFiles(d) {
+        // Make the DataTable for each file
+        // make buttons have the same style as other buttons
+        $.fn.dataTable.Buttons.defaults.dom.button.className = 'button';
+        tableInitOptions.data = d;
+        if (uiState.isLoggedIn) {
+            tableInitOptions.language = {emptyTable: "Uploaded files will appear here. Click \"Upload\" to get started"};
         } else {
-                        // only open the file editor when there is one file
-                        const dash = uppy.getPlugin("Dashboard");
-                        dash.toggleFileCard(true, file.id);
+            tableInitOptions.language = {emptyTable: "You are not logged in, please navigate to \"My Data\" > \"My Sessions\" and log in or create an account to begin uploading files"};
         }
-                });
-                this.uppy.on("file-removed", (file) => {
-                    // remove the batch change selects if now <2 files present
-                    if (this.uppy.getFiles().length < 2) {
-                        this.removeBatchSelectsFromDashboard();
+        DataTable.feature.register('quota', function(settings, opts) {
+            let options = Object.assign({option1: false, option2: false}, opts);
+            let container = document.createElement("div");
+            if (uiState.isLoggedIn) {
+                container.textContent = `Using ${prettyFileSize(uiState.userQuota)} of ${prettyFileSize(uiState.maxQuota)}`;
             }
+            return container;
         });
-
-                this.uppy.on("dashboard:modal-open", () => {
-                    // check if there were already files chosen from before:
-                    if (this.uppy.getFiles().length > 2) {
-                        this.addBatchSelectsToDashboard();
-                    }
-                    if (this.uppy.getFiles().length < 2) {
-                        this.removeBatchSelectsFromDashboard();
-                    }
+        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();
             });
-                this.uppy.on("dashboard:modal-closed", () => {
-                    if (this.uppy.getFiles().length < 2) {
-                        this.removeBatchSelectsFromDashboard();
-                    }
-                    let allFiles = this.uppy.getFiles();
-                    let completeFiles = this.uppy.getFiles().filter((f) => f.progress.uploadComplete === true);
-                    if (allFiles.length === completeFiles.length) {
-                        this.uppy.clear();
+        } else {
+            table.buttons(".uploadButton").disable();
         }
+        table.on("select", function(e, dt, type, indexes) {
+            indexes.forEach(function(i) {
+                doRowSelect(e.type, dt, i);
             });
-                this.uppy.on("dashboard:file-edit-start", (file) => {
-                    autocompletes[`${file.name}DbInput`] = false;
         });
-
-                this.uppy.on("dashboard:file-edit-complete", (file) => {
-                    // check the filename and hubname metadata and warn the user
-                    // to edit them if they are wrong. unfortunately I cannot
-                    // figure out how to force the file card to re-toggle
-                    // and jump back into the editor from here
-                    if (file) {
-                        let fileNameMatch = file.meta.name.match(fileNameRegex);
-                        let parentDirMatch = file.meta.parentDir.match(parentDirRegex);
-                        const dash = uppy.getPlugin("Dashboard");
-                        if (!fileNameMatch || fileNameMatch[0] !== file.meta.name) {
-                            uppy.info(`Error: File name has special characters, please rename file: '${file.meta.name}' to only include alpha-numeric characters, period, dash, underscore or plus.`, 'error', 5000);
-                        }
-                        if (!parentDirMatch || parentDirMatch[0] !== file.meta.parentDir) {
-                            uppy.info(`Error: Hub name has special characters, please rename hub: '${file.meta.parentDir}' to only include alpha-numeric characters, period, dash, underscore, or plus.`, 'error', 5000);
-                        }
-                    }
+        table.on("deselect", function(e, dt, type, indexes) {
+            indexes.forEach(function(i) {
+                doRowSelect(e.type, dt, i);
             });
+        });
+        table.on("click", function(e) {
+            if (e.target.className !== "dt-select-checkbox") {
+                e.stopPropagation();
+                // we've clicked somewhere not on the checkbox itself, we need to:
+                // 1. open the directory if the clicked row is a directory
+                // 2. select the file if the clicked row is a regular file
+                let row = table.row(e.target);
+                let data = row.data();
+                if (data.children && data.children.length > 0) {
+                    dataTableShowDir(table, data.fileName, data.fullPath);
+                    dataTableCustomOrder(table, {"fullPath": data.fullPath});
+                    table.draw();
+                } else {
+                    if (row.selected()) {
+                        row.deselect();
+                        doRowSelect("deselect", table, row.index());
+                    } else {
+                        row.select();
+                        doRowSelect("select", table, row.index());
                     }
-            uninstall() {
-                // not really used because we aren't ever uninstalling the uppy instance
-                this.uppy.off("file-added");
-            }
-        }
-        let uppyOptions = {
-            trigger: ".uploadButton",
-            showProgressDetails: true,
-            note: "The UCSC Genome Browser is not a HIPAA compliant data store. Do not upload patient information or other sensitive data files here, as anyone with the URL can view them.",
-            meta: {"genome": null, "fileType": null},
-            restricted: {requiredMetaFields: ["genome"]},
-            closeModalOnClickOutside: true,
-            closeAfterFinish: true,
-            theme: 'auto',
-            metaFields: (file) => {
-                const fields = [{
-                    id: 'name',
-                    name: 'File name',
-                    render: ({value, onChange, required, form}, h) => {
-                        return h('input',
-                            {type: "text",
-                            value: value,
-                            class: "uppy-u-reset uppy-c-textInput uppy-Dashboard-FileCard-input",
-                            onChange: e => {
-                                onChange(e.target.value);
-                                file.meta.fileType = detectFileType(e.target.value);
-                                file.meta.name = e.target.value;
-                            },
-                            required: required,
-                            form: form,
                 }
-                        );
-                    },
-                },
-                {
-                    id: 'genome',
-                    name: 'Genome',
-                    render: ({value, onChange}, h) => {
-                        // keep these as a variable so we can init the autocompleteCat
-                        // code only after the elements have actually been rendered
-                        // there are multiple rendering passes and only eventually
-                        // do the elements actually make it into the DOM
-                        let ret = h('div', {
-                                class: "uppy-Dashboard-FileCard-label",
-                                style: "display: inline-block; width: 78%"
-                                },
-                            // first child of div
-                            "Select from popular assemblies:",
-                            // second div child
-                            h('select', {
-                                id: `${file.meta.name}DbSelect`,
-                                style: "margin-left: 5px",
-                                onChange: e => {
-                                    onChange(e.target.value);
-                                    file.meta.genome = e.target.value;
-                                    file.meta.genomeLabel = e.target.selectedOptions[0].label;
             }
-                                },
-                                makeGenomeSelectOptions(file.meta.genome, file.meta.genomeLabel).map( (genomeObj) => {
-                                    return h('option', {
-                                        value: genomeObj.value,
-                                        label: genomeObj.label,
-                                        selected: file.meta.genome !== null ? genomeObj.value === file.meta.genome : genomeObj.value === defaultDb()
-                                    });
-                                })
-                            ),
-                            h('p', {
-                                class: "uppy-Dashboard-FileCard-label",
-                                style: "display: block; width: 78%",
-                                }, "or search for your genome:"),
-                            // third div child
-                            h('input', {
-                                id: `${file.meta.name}DbInput`,
-                                type: 'text',
-                                class: "uppy-u-reset uppy-c-textInput uppy-Dashboard-FileCard-input",
-                            }),
-                            h('input', {
-                                id: `${file.meta.name}DbSearchButton`,
-                                type: 'button',
-                                value: 'search',
-                                style: "margin-left: 5px",
-                            })
-                        );
-                    let selectToChange = document.getElementById(`${file.meta.name}DbSelect`);
-                    if (selectToChange) {
-                        let justInitted = initAutocompleteForInput(`${file.meta.name}DbInput`, selectToChange);
-                        if (justInitted) {
-                            // only do this once per file
-                            document.getElementById(`${file.meta.name}DbSearchButton`)
-                                    .addEventListener("click", (e) => {
-                                        let inp = document.getElementById(`${file.meta.name}DbInput`).value;
-                                        let selector = `[id='${file.meta.name}DbInput']`;
-                                        $(selector).autocompleteCat("search", inp);
         });
+        return table;
     }
+
+    function handleGetFileList(jsonData, textStatus) {
+        _.assign(uiState, jsonData.userFiles);
+        if (uiState.fileList) {
+            parseFileListIntoHash(uiState.fileList);
         }
-                    return ret;
-                    }
-                },
-                {
-                    id: 'parentDir',
-                    name: 'Hub Name',
-                }];
-                return fields;
-            },
-            doneButtonHandler: function() {
-                uppy.clear();
-            },
-        };
+
+        // first add the top level directories/files
+        let table = showExistingFiles(uiState.fileList);
+        table.columns.adjust().draw();
+        uppy.use(Uppy.Dashboard, uppyOptions);
+
+        // define this in init so globals are available at runtime
         let tusOptions = {
             endpoint: getTusdEndpoint(),
             withCredentials: true,
             retryDelays: null,
         };
-        uppy.use(Uppy.Dashboard, uppyOptions);
+
         uppy.use(Uppy.Tus, tusOptions);
         uppy.use(BatchChangePlugin, {target: Uppy.Dashboard});
         uppy.on('upload-success', (file, response) => {
             const metadata = file.meta;
             const d = new Date(metadata.lastModified);
             const pad = (num) => String(num).padStart(2, '0');
             const dFormatted = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
             const now = new Date(Date.now());
             const nowFormatted = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
             let newReqObj, hubTxtObj, parentDirObj;
             newReqObj = {
                 "fileName": cgiEncode(metadata.fileName),
                 "fileSize": metadata.fileSize,
                 "fileType": metadata.fileType,
                 "genome": metadata.genome,
@@ -1424,19 +1413,60 @@
                 "parentDir": "",
                 "fullPath": cgiEncode(metadata.parentDir),
             };
             // package the three objects together as one "hub" and display it
             let hub = [parentDirObj, newReqObj];
             if (hubTxtObj) {
                 hub.push(hubTxtObj);
             }
             addNewUploadedHubToTable(hub);
         });
         uppy.on('complete', (result) => {
             history.replaceState(uiState, "", document.location.href);
             console.log("replace history with uiState");
         });
     }
+
+    function checkJsonData(jsonData, callerName) {
+        // Return true if jsonData isn't empty and doesn't contain an error;
+        // otherwise complain on behalf of caller.
+        if (! jsonData) {
+            alert(callerName + ': empty response from server');
+        } else if (jsonData.error) {
+            console.error(jsonData.error);
+            alert(callerName + ': error from server: ' + jsonData.error);
+        } else if (jsonData.warning) {
+            alert("Warning: " + jsonData.warning);
+            return true;
+        } else {
+            if (debugCartJson) {
+                console.log('from server:\n', jsonData);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    function handleRefreshState(jsonData, textStatus) {
+        if (checkJsonData(jsonData, 'handleRefreshState')) {
+            handleGetFileList(jsonData, true);
+        }
+    }
+
+    function handleErrorState(jqXHR, textStatus) {
+        cart.defaultErrorCallback(jqXHR, textStatus);
+    }
+
+    function init() {
+        cart.setCgi('hgHubConnect');
+        cart.debug(debugCartJson);
+        // get the file list immediately upon page load
+        cart.send({ getHubSpaceUIState: {}}, handleRefreshState, handleErrorState);
+        cart.flush();
+    }
     return { init: init,
              uiState: uiState,
+             defaultDb: defaultDb,
+             makeGenomeSelectOptions: makeGenomeSelectOptions,
+             detectFileType: detectFileType,
            };
 }());