2f0d3fa3d2bcbf3e23e834ca68565c470f73939d chmalee Mon Oct 21 12:32:08 2024 -0700 Back end for auto-generating a hub with every user upload done diff --git src/hg/js/hgMyData.js src/hg/js/hgMyData.js index 28442aa..a9ad6f1 100644 --- src/hg/js/hgMyData.js +++ src/hg/js/hgMyData.js @@ -199,30 +199,43 @@ 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 rowDivs = document.querySelectorAll("[id^='" + req.name+"']"); let delIcon = rowDivs[0].previousElementSibling; rowDivs.forEach((div) => {div.remove();}); delIcon.remove(); // if nothing else we can close the dialog if (uiState.pendingQueue.length === 0) { + // first remove the grid headers + let headerEle = document.querySelectorAll(".fileListHeader"); + headerEle.forEach( (header) => { + if (header.style.display !== "none") { + header.style.display = "none"; + } + }); + //check if we can remove the batch change selects + if (uiState.pendingQueue.length > 1) { + document.querySelectorAll("[id^=batchChangeSelect]").forEach( (select) => { + select.remove(); + }); + } $("#filePickerModal").dialog("close"); } const d = new Date(req.lastModified); newReqObj = { "uploadTime": Date.now(), "lastModified": d.toJSON(), "fileName": metadata.fileName, "fileSize": metadata.fileSize, "fileType": metadata.fileType, "genome": metadata.genome, "hub": "" }; addNewUploadedFileToTable(newReqObj); }; @@ -288,30 +301,33 @@ let genomeInp = document.createElement("select"); genomeInp.classList.add("genomePicker"); genomeInp.name = `${fileName}#genomeInput`; genomeInp.id = `${fileName}#genomeInput`; 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"]; let cartChoice = document.createElement("option"); cartChoice.id = cartDb; cartChoice.label = cartDb; cartChoice.value = cartDb.split(" ").slice(-1); + if (cartChoice.value.startsWith("hub_")) { + cartChoice.label = cartDb.split(" ").slice(0,-1).join(" "); // take off the actual db value + } cartChoice.selected = true; genomeInp.appendChild(cartChoice); choices.forEach( (e) => { if (e === cartDb) {return;} // don't print the cart database twice let choice = document.createElement("option"); choice.id = e; choice.label = e; choice.value = e.split(" ")[1]; genomeInp.appendChild(choice); }); return genomeInp; } function makeTypeSelect(fileName) { let typeInp = document.createElement("select"); @@ -334,57 +350,90 @@ choice.id = e; choice.label = e; choice.value = e; typeInp.appendChild(choice); }); return typeInp; } function createTypeAndDbDropdown(fileName) { typeInp = makeTypeSelect(fileName); genomeInp = makeGenomeSelect(fileName); return [typeInp, genomeInp]; } + function deletePickedFile(eventInst) { + // called when the trash icon has been clicked to remove a file + // the sibling text content is the file name, which we use to + // find all the other elements to delete + let trashIconDiv = eventInst.currentTarget; + fname = trashIconDiv.nextSibling.textContent; + document.querySelectorAll("[id^='" + fname + "']").forEach( (sib) => { + sib.remove(); + }); + trashIconDiv.remove(); + delete uiState.toUpload[fname]; + // if there are no file rows left (1 row for the header) in the picker hide the headers: + let container = document.getElementById("fileList"); + if (getComputedStyle(container).getPropertyValue("grid-template-rows").split(" ").length === 1) { + let headerEle = document.querySelectorAll(".fileListHeader"); + headerEle.forEach( (header) => { + if (header.style.display !== "none") { + header.style.display = "none"; + } + }); + } + // if there is only one file picked hide the batch change selects, we may or may not have + // hidden the headers yet, so we should have 3 or less rows + if (getComputedStyle(container).getPropertyValue("grid-template-rows").split(" ").length <= 3) { + let batchChange = document.querySelectorAll("[id^=batchChangeSelect]"); + batchChange.forEach( (select) => { + select.remove(); + }); + } + } + function listPickedFiles() { // displays the users chosen files in a grid: if (uiState.input.files.length === 0) { console.log("not input"); return; } else { let displayList = document.getElementById("fileList"); let deleteEle = document.createElement("div"); deleteEle.classList.add("deleteFileIcon"); deleteEle.innerHTML = "<svg xmlns='http://www.w3.org/2000/svg' height='0.8em' viewBox='0 0 448 512'><!--! Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d='M135.2 17.7C140.6 6.8 151.7 0 163.8 0H284.2c12.1 0 23.2 6.8 28.6 17.7L320 32h96c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 96 0 81.7 0 64S14.3 32 32 32h96l7.2-14.3zM32 128H416V448c0 35.3-28.7 64-64 64H96c-35.3 0-64-28.7-64-64V128zm96 64c-8.8 0-16 7.2-16 16V432c0 8.8 7.2 16 16 16s16-7.2 16-16V208c0-8.8-7.2-16-16-16zm96 0c-8.8 0-16 7.2-16 16V432c0 8.8 7.2 16 16 16s16-7.2 16-16V208c0-8.8-7.2-16-16-16zm96 0c-8.8 0-16 7.2-16 16V432c0 8.8 7.2 16 16 16s16-7.2 16-16V208c0-8.8-7.2-16-16-16z'/></svg>"; for (let file of uiState.input.files ) { if (file.name in uiState.toUpload) { continue; } // create a list for the user to see let nameCell = document.createElement("div"); nameCell.classList.add("pickedFile"); nameCell.id = `${file.name}#fileName`; nameCell.textContent = `${file.name}`; // Add the form controls for this file: let [typeCell, dbCell] = createTypeAndDbDropdown(file.name); let sizeCell = document.createElement("div"); sizeCell.classList.add("pickedFile"); sizeCell.id = `${file.name}#fileSize`; sizeCell.textContent = prettyFileSize(file.size); - displayList.appendChild(deleteEle.cloneNode(true)); + newDelIcon = deleteEle.cloneNode(true); + newDelIcon.addEventListener("click", deletePickedFile); + displayList.appendChild(newDelIcon); displayList.appendChild(nameCell); displayList.appendChild(typeCell); displayList.appendChild(dbCell); displayList.appendChild(sizeCell); // finally add it for us uiState.toUpload[file.name] = file; } let headerEle = document.querySelectorAll(".fileListHeader"); headerEle.forEach( (header) => { if (header.style.display !== "block") { header.style.display = "block"; } }); if (Object.keys(uiState.toUpload).length > 1) { @@ -411,102 +460,92 @@ batchDb.addEventListener("change", function(e) { let newVal = e.currentTarget.selectedOptions[0].value; document.querySelectorAll("[id$=genomeInput]").forEach( (i) => { if (i === e.currentTarget) { return; } i.value = newVal; }); }); // append to the document displayList.appendChild(batchType); displayList.appendChild(batchDb); } document.querySelectorAll(".deleteFileIcon").forEach( (i) => { - i.addEventListener("click", function(e) { - // the sibling text content is the file name, which we use to - // find all the other elements to delete - let trashIconDiv = e.currentTarget; - fname = trashIconDiv.nextSibling.textContent; - document.querySelectorAll("[id^='" + fname + "']").forEach( (sib) => { - sib.remove(); - }); - trashIconDiv.remove(); - delete uiState.toUpload[fname]; - // if there are no rows left in the picker hide the headers: - let container = document.getElementById("fileList"); - if (getComputedStyle(container).getPropertyValue("grid-template-rows").split(" ").length === 1) { - let headerEle = document.querySelectorAll(".fileListHeader"); - headerEle.forEach( (header) => { - if (header.style.display !== "none") { - header.style.display = "none"; - } - }); - } - }); }); } // always clear the input element uiState.input = createInput(); } function dataTablePrintSize(data, type, row, meta) { return prettyFileSize(data); } + function dataTablePrintGenome(data, type, row, meta) { + if (data.startsWith("hub_")) + return data.split("_").slice(2).join("_"); + return data; + } + 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(fname, fileType) { + function deleteFile(fname, fileType, hubNameList) { // 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, fileType]}}); + // a little complex, but the format is: + // {commandToCgi: {arg: val, ...} + // but we make val an object as well, becoming: + // {commandToCgi: {fileList: [{propertyName1: propertyVal1, ...}, {propertName2: ...}]}} + cart.send({deleteFile: {fileList: [{fileName: fname, fileType: fileType, hubNameList: hubNameList}]}}); cart.flush(); deleteFileFromTable(fname); } function deleteFileList() { } function addFileToHub(rowData) { // a file has been uploaded and a hub has been created, present a modal // to choose which hub to associate this track to // backend wise: move the file into the hub directory // update the hubSpace row with the hub name // frontend wise: move the file row into a 'child' of the hub row console.log(`sending addToHub req for ${rowData.fileName} to `); cart.setCgi("hgHubConnect"); cart.send({addToHub: {hubName: "", dataFile: ""}}); cart.flush(); } - function viewInGenomeBrowser(fname, ftype, genome) { + function viewInGenomeBrowser(fname, ftype, genome, hubName) { // redirect to hgTracks with this track open in the hub if (typeof uiState.userUrl !== "undefined" && uiState.userUrl.length > 0) { if (ftype in extensionMap) { // 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); + let url = "../cgi-bin/hgTracks?hgsid=" + getHgsid() + "&db=" + genome + "&hubUrl=" + uiState.userUrl + hubName + "/hub.txt"; + window.location.assign(url); 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).order([8, 'asc']).draw().node(); $(newRow).css('color','red').animate({color: 'black'}, 1000); } else { showExistingFiles([req]); @@ -544,90 +583,98 @@ if (!("Cancel" in currBtns)) { currBtns.Cancel = function() { clearPickedFiles(); $(this).dialog("close"); }; $("#filePickerModal").dialog("option", "buttons", currBtns); } }, /* "Cancel": function() { clearPickedFiles(); $(this).dialog("close"); }, */ "Close": function() { + // delete everything that isn't the headers, which we set to hide: + let fileList = document.getElementById("fileList"); + let headers = document.querySelectorAll(".fileListHeader"); + fileList.replaceChildren(...headers); + headers.forEach( (header) => { + header.style.display = "none"; + }); + uiState.input = createInput(); $(this).dialog("close"); } }; $("#filePickerModal").dialog({ modal: true, buttons: hubUploadButtons, minWidth: $("#filePickerModal").width(), width: (window.innerWidth * 0.8), height: (window.innerHeight * 0.55), title: "Upload track data", open: function(e, ui) { $(e.target).parent().css("position", "fixed"); $(e.target).parent().css("top", "10%"); }, }); $("#filePickerModal").dialog("open"); } let tableInitOptions = { - language: { - emptyTable: "Uploaded files will appear here. Click \"Upload\" to get started", - }, layout: { topStart: { buttons: [ - {text: 'Upload', - action: startUploadDialog}, + { + text: 'Upload', + action: startUploadDialog, + enabled: false, // disable by default in case user is not logged in + }, ] } }, 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(row.fileName, row.fileType); + deleteFile(row.fileName, row.fileType, row.hub); }); // 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.fileType, row.genome); + viewInGenomeBrowser(row.fileName, row.fileType, row.genome, row.hub); }); // 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() { @@ -637,55 +684,71 @@ container.appendChild(delBtn); container.appendChild(viewBtn); container.appendChild(renameBtn); container.appendChild(addToHubBtn); return container; } }, { targets: 3, render: function(data, type, row) { return dataTablePrintSize(data); } }, { + targets: 5, + render: function(data, type, row) { + return dataTablePrintGenome(data); + } + }, + { // The upload time column, not visible but we use it to sort on new uploads targets: 8, visible: false, searchable: false } ], 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: "genome", title: "Genome", render: dataTablePrintGenome}, {data: "hub", title: "Hubs"}, {data: "lastModified", title: "File Last Modified"}, {data: "uploadTime", title: "Upload Time"}, ], order: [[6, 'desc']], + drawCallback: function(settings) { + if (isLoggedIn) { + settings.api.buttons(0).enable(); + } + } }; 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 (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"}; + } let table = new DataTable("#filesTable", tableInitOptions); } function showExistingHubs(d) { // Add the hubs to the files table if (!d) {return;} 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, @@ -762,43 +825,39 @@ 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 fileDiv = document.getElementById('filesDiv'); - if (typeof userFiles !== 'undefined') { + if (typeof userFiles !== 'undefined' && Object.keys(userFiles).length > 0) { uiState.fileList = userFiles.fileList; uiState.hubList = userFiles.hubList; uiState.userUrl = userFiles.userUrl; } - showExistingFiles(uiState.fileList); - showExistingHubs(uiState.hubList); + showExistingFiles(uiState.fileList.filter((row) => row.fileType !== "hub")); 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, }; }());