diff options
author | Frederick Muriuki Muriithi | 2024-06-27 10:53:54 -0500 |
---|---|---|
committer | Frederick Muriuki Muriithi | 2024-06-27 10:53:54 -0500 |
commit | df9da3d5b5e4382976ede1b54eb1aeb04c4c45e5 (patch) | |
tree | 1049a365445628145be075e761ffbe4897295ecc | |
parent | 9a8dddab072748a70d43416ac8e6db69ad6fb0cb (diff) | |
download | gn-uploader-df9da3d5b5e4382976ede1b54eb1aeb04c4c45e5.tar.gz |
Upload chunking: Provide UI and code for drag&drop and chunking
* Use resumable.js to handle the drag-and-drop feature, and chunking.
* Add styling for the drag-and-drop area, and provide visual
indication when a file is successfully dropped and added to the
upload list.
-rw-r--r-- | qc_app/templates/rqtl2/upload-rqtl2-bundle-step-01.html | 214 |
1 files changed, 188 insertions, 26 deletions
diff --git a/qc_app/templates/rqtl2/upload-rqtl2-bundle-step-01.html b/qc_app/templates/rqtl2/upload-rqtl2-bundle-step-01.html index fc87d5b..b9320ea 100644 --- a/qc_app/templates/rqtl2/upload-rqtl2-bundle-step-01.html +++ b/qc_app/templates/rqtl2/upload-rqtl2-bundle-step-01.html @@ -5,38 +5,92 @@ {%block title%}Upload R/qtl2 Bundle{%endblock%} {%block contents%} +{%macro rqtl2_file_help()%} +<span class="form-text text-muted"> + <p> + Provide a valid R/qtl2 zip file here. In particular, ensure your zip bundle + contains exactly one control file and the corresponding files mentioned in + the control file. + </p> + <p> + The control file can be either a YAML or JSON file. <em>ALL</em> other data + files in the zip bundle should be CSV files. + </p> + <p>See the + <a href="https://kbroman.org/qtl2/assets/vignettes/input_files.html" + target="_blank"> + R/qtl2 file format specifications + </a> + for more details. + </p> +</span> +{%endmacro%} {{upload_progress_indicator()}} +<div id="resumable-file-display-template" + class="panel panel-info" + style="display: none"> + <div class="panel-heading"></div> + <div class="panel-body"></div> +</div> + + <h2 class="heading">Upload R/qtl2 Bundle</h2> +<div id="resumable-drop-area" + style="display:none;background:#eeeeee;min-height:12em;border-radius:0.5em;padding:1em;"> + <p> + <a id="resumable-browse-button" href="#" + class="btn btn-info">Browse</a> + </p> + <p class="form-text text-muted"> + You can drag and drop your file here, or click the browse button. + Click on the file to remove it. + </p> + {{rqtl2_file_help()}} + <div id="resumable-selected-files" + style="display:flex;flex-direction:row;flex-wrap: wrap;justify-content:space-around;gap:10px 20px;"></div> + <div id="resumable-class-buttons" style="text-align: right;"> + <button id="resumable-upload-button" + class="btn btn-primary" + style="display: none">start upload</button> + <button id="resumable-cancel-upload-button" + class="btn btn-danger" + style="display: none">cancel upload</button> + </div> + <div class="progress" style="display: none"> + <div class="progress-bar" + role="progress-bar" + aria-valuenow="60" + aria-valuemin="0" + aria-valuemax="100" + style="width: 60%;"> + Uploading: 60% + </div> + </div> +</div> + <form id="frm-upload-rqtl2-bundle" action="{{url_for('upload.rqtl2.upload_rqtl2_bundle', species_id=species.SpeciesId, population_id=population.InbredSetId)}}" method="POST" - enctype="multipart/form-data"> + enctype="multipart/form-data" + data-resumable-action="/no/such/endpoint"> <input type="hidden" name="species_id" value="{{species.SpeciesId}}" /> <input type="hidden" name="population_id" value="{{population.InbredSetId}}" /> {{flash_all_messages()}} - <div form-group> + <div class="form-group"> <legend class="heading">file upload</legend> <label for="file-rqtl2-bundle" class="form-label">R/qtl2 bundle</label> <input type="file" id="file-rqtl2-bundle" name="rqtl2_bundle_file" accept="application/zip, .zip" required="required" class="form-control" /> - <span class="form-text text-muted"><p>Provide a valid R/qtl2 zip file here. In - particular, ensure your zip bundle contains exactly one control file and - the corresponding files mentioned in the control file.</p> - <p>The control file can be either a YAML or JSON file. <em>ALL</em> other - data files in the zip bundle should be CSV files.</p> - <p>See the - <a href="https://kbroman.org/qtl2/assets/vignettes/input_files.html" - target="_blank"> - R/qtl2 file format specifications</a> for more details.</p></span> + {{rqtl2_file_help()}} </div> <button type="submit" @@ -48,22 +102,130 @@ {%endblock%} {%block javascript%} +<script src="{{url_for('base.node_modules', + filename='resumablejs/resumable.js')}}"></script> <script type="text/javascript" src="/static/js/upload_progress.js"></script> <script type="text/javascript"> - setup_upload_handlers( - "frm-upload-rqtl2-bundle", make_data_uploader( - function (form) { - var formdata = new FormData(); - formdata.append( - "species_id", - form.querySelector('input[name="species_id"]').value); - formdata.append( - "population_id", - form.querySelector('input[name="population_id"]').value); - formdata.append( - "rqtl2_bundle_file", - form.querySelector("#file-rqtl2-bundle").files[0]); - return formdata; - })); + function readBinaryFile(file) { + return new Promise((resolve, reject) => { + var _reader = new FileReader(); + _reader.onload = (event) => {resolve(_reader.result);}; + _reader.readAsArrayBuffer(file); + }); + } + + function computeFileChecksum(file) { + return readBinaryFile(file) + .then((content) => { + return window.crypto.subtle.digest( + "SHA-256", new Uint8Array(content)); + }).then((digest) => { + return Uint8ArrayToHex(new Uint8Array(digest)) + }); + } + + function Uint8ArrayToHex(arr) { + var toHex = (val) => { + _hex = val.toString(16); + if(_hex.length < 2) { + return "0" + val; + } + return _hex; + }; + _hexstr = "" + arr.forEach((val) => {_hexstr += toHex(val)}); + return _hexstr + } + + var r = Resumable({ + target: $("#frm-upload-rqtl2-bundle").attr("data-resumable-action"), + fileType: ["zip"], + maxFiles: 1, + forceChunkSize: true, + generateUniqueIdentifier: (file, event) => { + return computeFileChecksum(file).then((checksum) => { + var _relativePath = (file.webkitRelativePath + || file.relativePath + || file.fileName + || file.name); + return checksum + "-" + _relativePath.replace( + /[^a-zA-Z0-9_-]/img, ""); + }); + } + }); + + if(r.support) { + //Hide form and display drag&drop UI + $("#frm-upload-rqtl2-bundle").css("display", "none"); + $("#resumable-drop-area").css("display", "block"); + + // Define UI elements for browse and drag&drop + r.assignDrop(document.getElementById("resumable-drop-area")); + r.assignBrowse(document.getElementById("resumable-browse-button")); + + // Event handlers + r.on("filesAdded", function(files) { + displayArea = $("#resumable-selected-files") + displayArea.empty(); + files.forEach((file) => { + var displayElement = $( + "#resumable-file-display-template").clone(); + displayElement.removeAttr("id"); + displayElement.css("display", ""); + displayElement.find(".panel-heading").text(file.fileName); + list = $("<ul></ul>"); + list.append($("<li><strong>Name</strong>: " + + (file.name + || file.fileName + || file.relativePath + || file.webkitRelativePath) + + "</li>")); + list.append($("<li><strong>Size</strong>: " + + (file.size / (1024*1024)).toFixed(2) + + " MB</li>")); + list.append($("<li><strong>Unique Identifier</strong>: " + + file.uniqueIdentifier + "</li>")); + list.append($("<li><strong>Mime</strong>: " + + file.file.type + + "</li>")); + displayElement.find(".panel-body").append(list); + displayElement.appendTo("#resumable-selected-files"); + $("#resumable-upload-button").css("display", ""); + $("#resumable-upload-button").on("click", (event) => { + r.upload() + }); + }); + }); + + r.on("uploadStart", (event) => { + $("#resumable-upload-button").css("display", "none"); + $("#resumable-cancel-upload-button").css("display", ""); + $("#resumable-cancel-upload-button").on("click", (event) => { + r.files.forEach((file) => { + if(file.isUploading()) { + file.abort(); + } + }); + $("#resumable-cancel-upload-button").css("display", "none"); + $("#resumable-upload-button").css("display", ""); + }); + }); + } else { + setup_upload_handlers( + "frm-upload-rqtl2-bundle", make_data_uploader( + function (form) { + var formdata = new FormData(); + formdata.append( + "species_id", + form.querySelector('input[name="species_id"]').value); + formdata.append( + "population_id", + form.querySelector('input[name="population_id"]').value); + formdata.append( + "rqtl2_bundle_file", + form.querySelector("#file-rqtl2-bundle").files[0]); + return formdata; + })); + } </script> {%endblock%} |