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 element for picking files from the users machine
+ pickedList: null, // the
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);
};