979ae40501044410b8375bdb5840dcdfcb70c712 chmalee Wed Oct 2 12:26:27 2024 -0700 Working hub deletion diff --git src/hg/js/hgMyData.js src/hg/js/hgMyData.js index 1aa42f2..3f77a59 100644 --- src/hg/js/hgMyData.js +++ src/hg/js/hgMyData.js @@ -1,730 +1,708 @@ /* jshint esnext: true */ debugCartJson = true; function prettyFileSize(num) { if (!num) {return "n/a";} 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 hidden input element pickedList: null, // the <div> for displaying files in toUpload pendingQueue: [], // our queue of pending [tus.Upload, file], kind of like the toUpload object fileList: [], // the files this user has uploaded, initially populated by the server // on page load, but gets updated as the user uploades/deletes files hubList: [], // the hubs this user has created/uploaded, initially populated by server // on page load, but gets updated as the user creates/deletes hubs userUrl: "", // the web accesible path where the uploads are stored for this user }; // We can use XMLHttpRequest if necessary or a mirror can't use tus var useTus = tus.isSupported && true; 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-hubspace.gi.ucsc.edu/files"; } 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); removeClearSubmitButtons(); } 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 createInput() { /* Create a new input element for a file picker */ let input = document.createElement("input"); input.multiple = true; input.type = "file"; input.id = "hiddenFileInput"; input.style = "display: none"; input.addEventListener("change", listPickedFiles); return input; } 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._req !== null) { xreq = req._req._xhr; if (xreq.readyState != XMLHttpRequest.DONE && xreq.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 [req, f] = uiState.pendingQueue.pop(); // we only need to abort requests that haven't finished yet if (req._req !== null) { if (req._req._xhr.readyState != XMLHttpRequest.DONE) { req.abort(true); } } 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 submitPickedFiles() { let tusdServer = getTusdEndpoint(); let onBeforeRequest = function(req) { let xhr = req.getUnderlyingObject(req); xhr.withCredentials = true; }; let onSuccess = function(req, metadata) { // remove the selected file from the input element and the ul list // FileList is a read only setting, so we have to make // a new one without this req delete uiState.toUpload[req.name]; let i, newReqObj = {}; for (i = 0; i < uiState.pendingQueue.length; i++) { fname = uiState.pendingQueue[i][1].name; if (fname === req.name) { // remove the successful tusUpload off uiState.pendingQueue.splice(i, 1); } } // remove the file from the list the user can see let li = document.getElementById(req.name+"#li"); li.parentNode.removeChild(li); if (uiState.pendingQueue.length === 0) { removeCancelAllButton(); } const d = new Date(req.lastModified); newReqObj = { "createTime": d.toJSON(), "fileName": metadata.fileName, "fileSize": metadata.fileSize, "fileType": metadata.fileType, "genome": metadata.genome, "hub": "" }; addNewUploadedFileToTable(newReqObj); }; let onError = function(err) { removeCancelAllButton(); if (err.originalResponse !== null) { alert(err.originalResponse._xhr.responseText); } else { alert(err); } }; let onProgress = function(bytesSent, bytesTotal) { this.updateProgress((bytesSent / bytesTotal) * 100); }; for (let f in uiState.toUpload) { file = uiState.toUpload[f]; if (useTus) { let progMeter = makeNewProgMeter(file.name); let metadata = { fileName: file.name, fileSize: file.size, fileType: document.getElementById(`${file.name}#typeInput`).selectedOptions[0].value, genome: document.getElementById(`${file.name}#genomeInput`).selectedOptions[0].value, lastModified: file.lastModified, }; let tusOptions = { endpoint: tusdServer, metadata: metadata, onProgress: onProgress.bind(progMeter), onBeforeRequest: onBeforeRequest, onSuccess: onSuccess.bind(null, file, metadata), onError: onError, retryDelays: null, }; // 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); uiState.pendingQueue.push([tusUpload, file]); 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 = createInput(); uiState.toUpload = {}; togglePickStateMessage(true); } function addClearSubmitButtons() { let firstBtn = document.getElementById("btnForInput"); let btnRow = document.getElementById("chooseAndSendFilesRow"); if (!document.getElementById("clearPicked")) { let clearBtn = document.createElement("button"); clearBtn.classList.add("button"); clearBtn.id = "clearPicked"; clearBtn.textContent = "Clear"; clearBtn.addEventListener("click", clearPickedFiles); btnRow.append(clearBtn); } if (!document.getElementById("submitPicked")) { submitBtn = document.createElement("button"); submitBtn.id = "submitPicked"; submitBtn.classList.add("button"); submitBtn.textContent = "Submit"; submitBtn.addEventListener("click", submitPickedFiles); btnRow.append(submitBtn); } } function removeClearSubmitButtons() { let btn = document.getElementById("clearPicked"); btn.parentNode.removeChild(btn); btn = document.getElementById("submitPicked"); btn.parentNode.removeChild(btn); } function makeGenomeSelect(formName, fileName) { let genomeInp = document.createElement("select"); genomeInp.classList.add("genomePicker"); genomeInp.name = `${fileName}#genomeInput`; genomeInp.id = `${fileName}#genomeInput`; genomeInp.form = formName; let labelChoice = document.createElement("option"); labelChoice.label = "Choose Genome"; labelChoice.value = "Choose Genome"; labelChoice.selected = true; labelChoice.disabled = true; genomeInp.appendChild(labelChoice); let choices = ["Human hg38", "Human T2T", "Human hg19", "Mouse mm39", "Mouse mm10"]; choices.forEach( (e) => { let choice = document.createElement("option"); choice.id = e; choice.label = e; choice.value = e.split(" ")[1]; genomeInp.appendChild(choice); }); return genomeInp; } function makeTypeSelect(formName, fileName) { let typeInp = document.createElement("select"); typeInp.classList.add("typePicker"); typeInp.name = `${fileName}#typeInput`; typeInp.id = `${fileName}#typeInput`; typeInp.form = formName; let labelChoice = document.createElement("option"); labelChoice.label = "Choose File Type"; labelChoice.value = "Choose File Type"; labelChoice.selected = true; labelChoice.disabled = true; typeInp.appendChild(labelChoice); let choices = ["hub.txt", "bigBed", "bam", "vcf", "bigWig"]; choices.forEach( (e) => { let choice = document.createElement("option"); choice.id = e; choice.label = e; choice.value = e; typeInp.appendChild(choice); }); return typeInp; } function makeFormControlsForFile(li, formName, fileName) { typeInp = makeTypeSelect(formName, fileName); genomeInp = makeGenomeSelect(formName, fileName); li.append(typeInp); li.append(genomeInp); } function listPickedFiles() { // let the user choose files: if (uiState.input.files.length === 0) { console.log("not input"); return; } else { let displayList; let displayListForm = document.getElementsByClassName("pickedFilesForm"); if (displayListForm.length === 0) { displayListForm = document.createElement("form"); displayListForm.id = "displayListForm"; displayListForm.classList.add("pickedFilesForm"); displayList = document.createElement("ul"); displayList.classList.add("pickedFiles"); displayListForm.appendChild(displayList); uiState.pickedList.appendChild(displayListForm); } else { displayList = displayListForm[0].firstChild; } 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)}`; // Add the form controls for this file: makeFormControlsForFile(li, "displayListForm", file.name); displayList.appendChild(li); // finally add it for us uiState.toUpload[file.name] = file; } togglePickStateMessage(false); addClearSubmitButtons(); } // always clear the input element uiState.input = createInput(); } function dataTablePrintSize(data, type, row, meta) { return prettyFileSize(data); } - function deleteFileFromTable(rowIx, fname) { + function deleteFileFromTable(fname) { // req is an object with properties of an uploaded file, make a new row // for it in the filesTable let table = $("#filesTable").DataTable(); let row = table.row((idx, data) => data.fileName === fname); row.remove().draw(); } - function deleteFile(rowIx, fname) { + function deleteFile(fname, fileType) { // Send an async request to hgHubConnect to delete the file // Note that repeated requests, like from a bot, will return 404 as a correct response console.log(`sending delete req for ${fname}`); cart.setCgi("hgHubConnect"); - cart.send({deleteFile: {fileNameList: [fname]}}); + cart.send({deleteFile: {fileNameList: [fname, fileType]}}); cart.flush(); - deleteFileFromTable(rowIx, fname); + deleteFileFromTable(fname); } function deleteFileList() { } function viewInGenomeBrowser(fname, genome) { // redirect to hgTracks with this track open in the hub if (typeof uiState.userUrl !== "undefined" && uiState.userUrl.length > 0) { bigBedExts = [".bb", ".bigBed", ".vcf.gz", ".vcf", ".bam", ".bw", ".bigWig"]; let i; for (i = 0; i < bigBedExts.length; i++) { if (fname.toLowerCase().endsWith(bigBedExts[i].toLowerCase())) { // TODO: tusd should return this location in it's response after // uploading a file and then we can look it up somehow, the cgi can // write the links directly into the html directly for prev uploaded files maybe? window.location.assign("../cgi-bin/hgTracks?db=" + genome + "&hgt.customText=" + uiState.userUrl + fname); return false; } } } } function addNewUploadedFileToTable(req) { // req is an object with properties of an uploaded file, make a new row // for it in the filesTable let table = null; if ($.fn.dataTable.isDataTable("#filesTable")) { table = $("#filesTable").DataTable(); let newRow = table.row.add(req).draw(); } else { showExistingFiles([req]); } } function createHubSuccess(jqXhr, textStatus) { console.log(jqXhr); $("#newTrackHubDialog").dialog("close"); addNewUploadedFileToTable({ createTime: jqXhr.creationTime, fileType: "hub", fileName: jqXhr.hubName, genome: jqXhr.db, fileSize: null, hub: jqXhr.hubName }); } function createHub(db, hubName) { // send a request to hgHubConnect to create a hub for this user cart.setCgi("hgHubConnect"); cart.send({createHub: {db: db, name: hubName}}, createHubSuccess, null); cart.flush(); } function startHubCreate() { // put up a dialog to walk a user through setting up a track hub console.log("create a hub button clicked!"); $("#newTrackHubDialog").dialog({ minWidth: $("#newTrackHubDialog").width(), }); // attach the event handler to save this hub to this users hubspace let saveBtn = document.getElementById("doNewCollection"); saveBtn.addEventListener("click", (e) => { let db = document.getElementById("db").value; let hubName = document.getElementById("hubName").value; // TODO: add a spinner while we wait for the request to complete createHub(db, hubName); }); $("#newTrackHubDialog").dialog("open"); } let tableInitOptions = { layout: { topStart: { buttons: [ {text: 'Create hub', action: startHubCreate}, ] } }, columnDefs: [ { orderable: false, targets: 0, title: "<input type=\"checkbox\"></input>", render: function(data, type, row) { return "<input type=\"checkbox\"></input>"; } }, { orderable: false, targets: 1, data: "action", title: "Action", render: function(data, type, row) { /* Return a node for rendering the actions column */ // all of our actions will be buttons in this div: let container = document.createElement("div"); // click to call hgHubDelete file let delBtn = document.createElement("button"); delBtn.textContent = "Delete"; delBtn.type = 'button'; delBtn.addEventListener("click", function() { - deleteFile(0, row.fileName); + deleteFile(row.fileName, row.fileType); }); // click to view hub/file in gb: let viewBtn = document.createElement("button"); viewBtn.textContent = "View in Genome Browser"; viewBtn.type = 'button'; viewBtn.addEventListener("click", function() { viewInGenomeBrowser(row.fileName, row.genome); }); // click to rename file or hub: let renameBtn = document.createElement("button"); renameBtn.textContent = "Rename"; renameBtn.type = 'button'; renameBtn.addEventListener("click", function() { console.log("rename btn clicked!"); }); // click to associate this track to a hub let addToHubBtn = document.createElement("button"); addToHubBtn.textContent = "Add to hub"; addToHubBtn.type = 'button'; addToHubBtn.addEventListener("click", function() { console.log("add to hub button clicked!"); }); container.appendChild(delBtn); container.appendChild(viewBtn); container.appendChild(renameBtn); container.appendChild(addToHubBtn); return container; } }, { targets: 3, render: function(data, type, row) { return dataTablePrintSize(data); } } ], columns: [ {data: "", }, {data: "", }, {data: "fileName", title: "File name"}, {data: "fileSize", title: "File size", render: dataTablePrintSize}, {data: "fileType", title: "File type"}, {data: "genome", title: "Genome"}, {data: "hub", title: "Hubs"}, {data: "createTime", title: "Creation Time"}, ], order: [[6, 'desc']], - /* - drawCallback: function(settings) { - // every time we draw we need to update event handlers if we've added/deleted a row - let table = this.DataTable(); - btns = document.querySelectorAll('.deleteFileBtn'); - let i, numRows= table.rows().data().length; - for (i = 0; i < numRows; i++) { - let fname = table.cell(i, 2).data(); - btns[i].addEventListener("click", (e) => { - deleteFile(i, fname); - }); - } - btns = document.querySelectorAll('.viewInBtn'); - for (i = 0; i < numRows; i++) { - let fname = table.cell(i, 2).data(); - let genome = table.cell(i, 5).data(); - btns[i].addEventListener("click", (e) => { - viewInGenomeBrowser(fname, genome); - }); - } - }, - */ }; 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; let table = new DataTable("#filesTable", tableInitOptions); let toRemove = document.getElementById("welcomeDiv"); if (d.length > 0 && toRemove !== null) { toRemove.remove(); } } function showExistingHubs(d) { // Add the hubs to the files table let table = $("#filesTable").DataTable(); d.forEach((hub) => { let hubName = hub.hubName; let db = hub.genome; let data = { fileName: hubName, fileSize: null, fileType: "hub", genome: db, hub: hubName, createTime: null, }; table.row.add(data).draw(); }); } 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); } 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 inputBtn = document.getElementById("btnForInput"); 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? } let parent = document.getElementById("chooseAndSendFilesRow"); let input = createInput(); uiState.input = input; inputBtn.parentNode.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); } _.assign(uiState,cartJson); 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(); // TODO: initialize buttons, check if there are already files // TODO: write functions for // after picking files // choosing file types // creating default trackDbs // editing trackDbs let welcomeDiv = document.createElement("div"); welcomeDiv.id = "welcomeDiv"; welcomeDiv.textContent = "Once files are uploaded they will display here. Click \"Choose files\" above or \"Create Hub\" below to get started"; let fileDiv = document.getElementById('filesDiv'); fileDiv.insertBefore(welcomeDiv, fileDiv.firstChild); if (typeof userFiles !== 'undefined' && (userFiles.fileList.length > 0 || userFiles.hubList.length > 0)) { uiState.fileList = userFiles.fileList; uiState.hubList = userFiles.hubList; uiState.userUrl = userFiles.userUrl; } showExistingFiles(uiState.fileList); showExistingHubs(uiState.hubList); inputBtn.addEventListener("click", (e) => uiState.input.click()); //uiState.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 } $("#newTrackHubDialog").dialog({ modal: true, autoOpen: false, title: "Create new track hub", closeOnEscape: true, minWidth: 400, minHeight: 120 }); } return { init: init, uiState: uiState, }; }()); // 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(); hubCreate.updateStateAndPage(event.state, false); };