dbc7d6bb2237141fdbea01fff6c265ade6738c57
chmalee
  Fri Oct 6 13:03:41 2023 -0700
Start on trackHubWizard module

diff --git src/hg/js/hgMyData.js src/hg/js/hgMyData.js
index e1bc897..39a0f6d 100644
--- src/hg/js/hgMyData.js
+++ src/hg/js/hgMyData.js
@@ -1,124 +1,419 @@
 /* jshint esnext: true */
-debugCartJson = false;
-var hgMyData = (function() {
-    let uiState = {};
+debugCartJson = true;
+
+function prettyFileSize(num) {
+    if (num < (1000 * 1024)) {
+        return `${(num/1000).toFixed(1)}kb`;
+    } else if (num < (1000 * 1000 * 1024)) {
+        return `${((num/1000)/1000).toFixed(1)}mb`;
+    } else {
+        return `${(((num/1000)/1000)/1000).toFixed(1)}gb`;
+    }
+}
+
+var hubCreate = (function() {
+    let uiState = { // our object for keeping track of the current UI and what to do
+        toUpload: {}, // set of file objects keyed by name
+        input: null, // the <input> element for picking files from the users machine
+        pickedList: null, // the <div> for displaying files in toUpload
+        pendingQueue: [], // our queue of pending [xmlhttprequests, file], kind of like the toUpload object
+    };
+
+    // We can use XMLHttpRequest if necessary or a mirror can't use tus
+    var useTus = tus.isSupported && typeof tusdPort !== 'undefined' && typeof tusdBasePath !== 'undefined';
+
+    function getTusdEndpoint() {
+        // return the port and basepath of the tusd server
+        // NOTE: the port and basepath are specified in hg.conf
+        let currUrl = parseUrl(window.location.href);
+        return "https://hgwdev-chmalee.gi.ucsc.edu" + ":" + tusdPort + "/" + tusdBasePath + "/";
+    }
+
+    function togglePickStateMessage(showMsg = false) {
+        if (showMsg) {
+            let para = document.createElement("p");
+            para.textContent = "No files selected for upload";
+            para.classList.add("noFiles");
+            uiState.pickedList.prepend(para);
+        } else {
+            let msg = document.querySelector(".noFiles");
+            if (msg) {
+                msg.parentNode.removeChild(msg);
+            }
+        }
+    }
+
+    function liForFile(file) {
+        let liId = `${file.name}#li`;
+        let li = document.getElementById(liId);
+        return li;
+    }
+
+    function newButton(text) {
+        /* Creates a new button with some text as the label */
+        let newBtn = document.createElement("label");
+        newBtn.classList.add("button");
+        newBtn.textContent = text;
+        return newBtn;
+    }
+
+    function requestsPending() {
+        /* Return true if requests are still pending, which means it needs to
+         * have been sent(). aborted requests are considered done for this purpose */
+        for (let [req, f] of uiState.pendingQueue) {
+            if (req.readyState != XMLHttpRequest.DONE && req.readyState != XMLHttpRequest.UNSENT) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    function addCancelButton(file, req) {
+        /* Add a button that cancels the request req */
+        let li = liForFile(file);
+        let newBtn = newButton("Cancel upload");
+        newBtn.addEventListener("click", (e) => {
+            req.abort();
+            li.removeChild(newBtn);
+            // TODO: make this remove the cancel all button if it's the last pending
+            // request
+            stillPending = requestsPending();
+            if (!stillPending) {
+                let btnRow = document.getElementById("chooseAndSendFilesRow");
+                cAllBtn = btnRow.lastChild;
+                btnRow.removeChild(cAllBtn);
+            }
+        });
+        li.append(newBtn);
+    }
+
+    function removeCancelAllButton() {
+        let btnRow = document.getElementById("chooseAndSendFilesRow");
+        if (btnRow.lastChild.textContent === "Cancel all") {
+            btnRow.removeChild(btnRow.lastChild);
+        }
+        togglePickStateMessage(true);
+    }
+
+    function addCancelAllButton() {
+        let btnRow = document.getElementById("chooseAndSendFilesRow");
+        let newBtn = newButton("Cancel all");
+        newBtn.addEventListener("click", (e) => {
+            while (uiState.pendingQueue.length > 0) {
+                let [q, f] = uiState.pendingQueue.pop();
+                // we only need to abort requests that haven't finished yet
+                if (q.readyState != XMLHttpRequest.DONE) {
+                    q.abort();
+                }
+                let li = liForFile(f);
+                if (li !== null) {
+                    // the xhr event handlers should handle this but just in case
+                    li.removeChild(li.lastChild);
+                }
+            }
+        });
+        btnRow.appendChild(newBtn);
+    }
+
+    function makeNewProgMeter(fileName) {
+        // create a progress meter for this filename
+        const progMeterWidth = 128;
+        const progMeterHeight = 12;
+        const progMeter = document.createElement("canvas");
+        progMeter.classList.add("upload-progress");
+        progMeter.setAttribute("width", progMeterWidth);
+        progMeter.setAttribute("height", progMeterHeight);
+        idStr = `${fileName}#li`;
+        ele = document.getElementById(idStr);
+        ele.appendChild(progMeter);
+        progMeter.ctx = progMeter.getContext('2d');
+        progMeter.ctx.fillStyle = 'orange';
+        progMeter.updateProgress = (percent) => {
+            // update the progress meter for this elem
+            if (percent === 100) {
+                progMeter.ctx.fillStyle = 'green';
+            }
+            progMeter.ctx.fillRect(0, 0, (progMeterWidth * percent) / 100, progMeterHeight);
+        };
+        progMeter.updateProgress(0);
+        return progMeter;
+    }
+
+
+    function sendFile(file) {
+        // this function can go away once tus is implemented
+        // this is mostly adapted from the mdn example:
+        // https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications#example_uploading_a_user-selected_file
+        // we will still need all the event handlers though
+        const xhr = new XMLHttpRequest();
+        this.progMeter = makeNewProgMeter(file.name);
+        this.xhr = xhr;
+        const endpoint = "../cgi-bin/hgHubConnect";
+        const self = this; // why do we need this?
+        const fd = new FormData();
+
+        this.xhr.upload.addEventListener("progress", (e) => {
+            if (e.lengthComputable) {
+                const pct = Math.round(e.loaded * 100) / e.total;
+                self.progMeter.updateProgress(pct);
+            }
+        }, false);
+
+        // loadend handles abort/error or load events
+        this.xhr.upload.addEventListener("loadend", (e) => {
+            this.progMeter.updateProgress();
+            const canvas = self.progMeter.ctx.canvas;
+            canvas.parentNode.removeChild(canvas);
+            delete uiState.toUpload[file.name];
+            let li = liForFile(file);
+            li.parentNode.removeChild(li);
+            if (Object.keys(uiState.toUpload).length === 0) {
+                removeCancelAllButton();
+            }
+        }, false);
+
+        // for now just treat it like an abort/error
+        this.xhr.upload.addEventListener("timeout", (e) => {
+            progMeter.updateProgress();
+            const canvas = self.progMeter.ctx.canvas;
+            canvas.parentNode.removeChild(canvas);
+        });
+
+        this.xhr.upload.addEventListener("load", (e) => {
+            // TODO:  on load populate the uploaded files side
+            let uploadSection = document.getElementById("uploadedFilesSection");
+            if (uploadSection.style.display === "none") {
+                uploadSection.style.display = "";
+            }
+        });
+
+        // on error keep the file name present and show the error somehow
+        this.xhr.upload.addEventListener("error", (e) => {
+        });
+        this.xhr.upload.addEventListener("abort", (e) => {
+            console.log("request aborted");
+        });
+        this.xhr.upload.addEventListener("timeout", (e) => {
+        });
+
+        // finally we can send the request
+        this.xhr.open("POST", endpoint, true);
+        fd.set("createHub", 1);
+        fd.set("userFile", file);
+        this.xhr.send(fd);
+        uiState.pendingQueue.push([this.xhr,file]);
+
+        addCancelButton(file, this.xhr);
+    }
+
+    function submitPickedFiles() {
+        let tusdServer = getTusdEndpoint();
+        for (let f in uiState.toUpload) {
+            file = uiState.toUpload[f];
+            if (useTus) {
+                let tusOptions = {
+                    endpoint: tusdServer,
+                    metadata: {
+                        filename: file.name,
+                        fileType: file.type,
+                        fileSize: file.size
+                    }
+                };
+                // TODO: get the uploadUrl from the tusd server
+                // use a pre-create hook to validate the user
+                // and get an uploadUrl
+                let tusUpload = new tus.Upload(file, tusOptions);
+                tusUpload.start();
+            } else {
+                // make a new XMLHttpRequest for each file, if tusd-tusclient not supported
+                new sendFile(file);
+            }
+        }
+        addCancelAllButton();
+        return;
+    }
+
+    function clearPickedFiles() {
+        while (uiState.pickedList.firstChild) {
+            uiState.pickedList.removeChild(uiState.pickedList.firstChild);
+        }
+        uiState.input.value = "";
+        uiState.toUpload = {};
+        togglePickStateMessage(true);
+    }
+
+    function addClearSubmitButtons() {
+        let firstBtn = document.getElementById("btnForInput");
+        let btnRow = document.getElementById("chooseAndSendFilesRow");
+        if (!document.getElementById("clearPicked")) {
+            let newLabel = document.createElement("label");
+            newLabel.classList.add("button");
+            newLabel.id = "clearPicked";
+            newLabel.textContent = "Clear";
+            newLabel.addEventListener("click", clearPickedFiles);
+            btnRow.append(newLabel);
+        }
+        if (!document.getElementById("submitPicked")) {
+            newLabel = document.createElement("label");
+            newLabel.id = "submitPicked";
+            newLabel.classList.add("button");
+            newLabel.textContent = "Submit";
+            newLabel.addEventListener("click", submitPickedFiles);
+            btnRow.append(newLabel);
+        }
+    }
+
+    function removeClearSubmitButtons() {
+        let btn = document.getElementById("clearPicked");
+        btn.parentNode.removeChild(btn);
+        btn = document.getElementById("submitPicked");
+        btn.parentNode.removeChild(btn);
+    }
+
+    function listPickedFiles() {
+        if (uiState.input.files.length === 0) {
+            togglePickStateMessage(true);
+            removeClearSubmitButtons();
+        } else {
+            let displayList = document.createElement("ul");
+            displayList.classList.add("pickedFiles");
+            uiState.pickedList.appendChild(displayList);
+            for (let file of uiState.input.files ) {
+                if (file.name in uiState.toUpload) { continue; }
+                // create a list for the user to see
+                let li = document.createElement("li");
+                li.classList.add("pickedFile");
+                li.id = `${file.name}#li`;
+                li.textContent = `File name: ${file.name}, file size: ${prettyFileSize(file.size)}`;
+                displayList.appendChild(li);
+
+                // finally add it for us
+                uiState.toUpload[file.name] = file;
+            }
+            togglePickStateMessage(false);
+            addClearSubmitButtons();
+        }
+    }
+
     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 updateStateAndPage(jsonData, doSaveHistory) {
         // Update uiState with new values and update the page.
         _.assign(uiState, jsonData);
         /*
-        db = uiState.db;
-        if (jsonData.positionMatches !== undefined) {
-            // clear the old resultHash
-            uiState.resultHash = {};
-            _.each(uiState.positionMatches, function(match) {
-                uiState.resultHash[match.name] = match;
-            });
-        } else {
-            // no results for this search
-            uiState.resultHash = {};
-            uiState.positionMatches = [];
-        }
-        updateFilters(uiState);
-        updateSearchResults(uiState);
-        buildSpeciesDropdown();
-        fillOutAssemblies();
         urlVars = {"db": db, "search": uiState.search, "showSearchResults": ""};
         // changing the url allows the history to be associated to a specific url
         var urlParts = changeUrl(urlVars);
-        $("#searchCategories").jstree(true).refresh(false,true);
         if (doSaveHistory)
             saveHistory(uiState, urlParts);
         changeSearchResultsLabel();
         */
     }
 
     function handleRefreshState(jsonData) {
         if (checkJsonData(jsonData, 'handleRefreshState')) {
             updateStateAndPage(jsonData, true);
         }
         $("#spinner").remove();
     }
 
     function init() {
         cart.setCgi('hgMyData');
         cart.debug(debugCartJson);
+        if (!useTus) {
+            console.log("tus is not supported, falling back to XMLHttpRequest");
+        }
+        let pickedFiles = document.getElementById("fileList");
+        let input = document.getElementById("uploadedFiles");
+        if (pickedFiles !== null) {
+            // this element should be an empty div upon loading the page
+            uiState.pickedList = pickedFiles;
+            if (pickedFiles.children.length === 0) {
+                let para = document.createElement("p");
+                para.textContent = "No files chosen yet";
+                para.classList.add("noFiles");
+                pickedFiles.appendChild(para);
+            }
+        } else {
+            // TODO: graceful handle of leaving the page and coming back?
+        }
+        if (input !== null) {
+            uiState.input = input;
+            uiState.input.style.float = "right";
+            uiState.input.style.opacity = 0;
+            uiState.input.value = "";
+        } else {
+            let parent = document.getElementById("chooseAndSendFilesRow");
+            input = document.createElement("input");
+            input.style.opacity = 0;
+            input.style.float = "right";
+            input.multiple = true;
+            input.id = "uploadedFiles";
+            input.type = "file";
+            uiState.input = input;
+            parent.appendChild(input);
+        }
+
         if (typeof cartJson !== "undefined") {
             if (typeof cartJson.warning !== "undefined") {
                 alert("Warning: " + cartJson.warning);
             }
             var urlParts = {};
             if (debugCartJson) {
                 console.log('from server:\n', cartJson);
             }
-            /*
-            if (typeof cartJson.search !== "undefined") {
-                urlParts = changeUrl({"search": cartJson.search});
-            } else {
-                urlParts = changeUrl({"db": db});
-                cartJson.search = urlParts.urlVars.search;
-            }
-            */
             _.assign(uiState,cartJson);
-            if (typeof cartJson.categs  !== "undefined") {
-                _.each(uiState.positionMatches, function(match) {
-                    uiState.resultHash[match.name] = match;
-                });
-                filtersToJstree();
-                makeCategoryTree();
-            } else {
-                cart.send({ getUiState: {} }, handleRefreshState);
-                cart.flush();
-            }
             saveHistory(cartJson, urlParts, true);
         } else {
             // no cartJson object means we are coming to the page for the first time:
-            cart.send({ getUiState: {} }, handleRefreshState);
-            cart.flush();
+            //cart.send({ getUiState: {} }, handleRefreshState);
+            //cart.flush();
+            // TODO: initialize buttons, check if there are already files
+            // TODO: write functions for
+            //     after picking files
+            //     choosing file types
+            //     creating default trackDbs
+            //     editing trackDbs
+            // TODO: make hgHubConnect respond to requests
+            // TODO: initialize tus-client
+            // TODO: get user name
+            // TODO: send a request with username
+            // TODO: have tusd respond on server
+            input.addEventListener("change", listPickedFiles);
+            // TODO: add event handler for when file is succesful upload
+            // TODO: add event handlers for editing defaults, grouping into hub
+            // TODO: display quota somewhere
+            // TODO: customize the li to remove the picked file
         }
-
-        // your code here
     }
     return { init: init,
+             uiState: uiState,
            };
 
 }());
 
-/*
-$(document).ready(function() {
-    $('#searchBarSearchString').bind('keypress', function(e) {  // binds listener to search button
-        if (e.which === 13) {  // listens for return key
-            e.preventDefault();   // prevents return from also submitting whole form
-            if ($("#searchBarSearchString").val() !== undefined) {
-                $('#searchBarSearchButton').focus().click(); // clicks search button button
-            }
-        }
-    });
-});
-*/
 
 // when a user reaches this page from the back button we can display our saved state
 // instead of sending another network request
 window.onpopstate = function(event) {
     event.preventDefault();
-    hgMyData.updateStateAndPage(event.state, false);
+    hubCreate.updateStateAndPage(event.state, false);
 };