--- a +++ b/qiita_pet/static/js/sampleTemplateVue.js @@ -0,0 +1,666 @@ +var sampleTemplatePage = null; + +// taken from https://stackoverflow.com/a/19963011 +function unique(array) { + return $.grep(array, function(el, index) { + return index == $.inArray(el, array); + }); +} + +function get_column_summary(study_id, portal, column, numSamples){ + var row = $('.' + column + 'collapsed'); + + if (row.is(":hidden")) { + var cell = $(row.children()[0]); + cell.html('<img src="' + portal + '/static/img/waiting.gif" style="display:block;margin-left: auto;margin-right: auto"/>'); + $.get(portal + '/study/description/sample_template/columns/', {study_id: study_id, column: column}, function(data) { + cell.html(''); + var values = data['values']; + var uniques = unique(values); + var table = $('<table>').addClass('table').appendTo(cell); + if (uniques.length === 1) { + var tr = $('<tr>').appendTo(table); + var td = $('<td>').append(uniques[0] + ' is repeated in all rows.').appendTo(tr); + } else if (uniques.length === numSamples) { + var tr = $('<tr>').appendTo(table); + var td = $('<td>').append('All the values in this category are different').appendTo(tr); + } else { + var counts = {}; + for (let u of uniques) { counts[u] = 0; } + for (let v of values) { counts[v]++; } + for (let u of uniques) { + var tr = $('<tr>').appendTo(table); + var td = $('<td>').append(u).appendTo(tr); + var td = $('<td>').append(counts[u]).appendTo(tr); + } + } + }) + } +} + +Vue.component('sample-template-page', { + template: '<div id="sample-template-main-div">' + + // Title div + '<div class="row">' + + '<div class="col-md-12">' + + '<h3>Sample Information<span id="title-h3"></span></h3>' + + '<h6>Last update: <span id="current-sample-info"></span></h6>' + + '</div>' + + '</div>' + + // Processing div + '<div class="row" id="st-processsing-div" style="border-radius: 10px; background: #EEE;">' + + '<div class="col-md-12">' + + '<h4>We are processing your request via job "<span id="job-id-span"></span>". Status: "<span id="job-status-span"></span>"</h4>' + + '</div>' + + '</div>' + + '</br>' + + // Content div + '<div class="row">' + + '<div class="col-md-12" id="sample-template-contents">' + + '</div>' + + '</div>' + + '</div>', + props: ['portal', 'study-id'], + maxDirectUploadSize: 10485760, + methods: { + /** + * + * Checks the status of the job performing some task over the sample + * information + * + **/ + checkJob: function() { + let vm = this; + $.get(vm.portal + '/study/process/job/', {job_id: vm.job}, function(data) { + var jobStatus; + jobStatus = data['job_status']; + $('#job-id-span').text(data['job_id']); + $('#job-status-span').text(jobStatus); + + if (jobStatus === 'error' || jobStatus === 'success') { + vm.stopJobCheckInterval(); + // Hide the processing div + $('#st-processsing-div').hide(); + // Enable interaction bits + $('#update-st-div').show(); + $('.st-interactive').prop('disabled', false); + if (jobStatus === 'error') { + // The job errored - show the error + bootstrapAlert(data['job_error'], "danger"); + } else { + // The job succeeded - reload the interface to show changes + if (vm.refresh) { + vm.refresh = false; + location.reload(); + } else { + vm.updateSampleTemplateOverview(); + } + } + } + }) + .fail(function(object, status, error_msg) { + bootstrapAlert("Error checking the job status: " + object.statusText, "danger"); + }); + }, + + /** + * + * Starts the interval that checks the status of the job. + * + **/ + startJobCheckInterval: function(jobId) { + let vm = this; + vm.job = jobId; + + // Hide the current error message (if any) + $('#alert-message').alert('close'); + // Show the processing div + $('#st-processsing-div').show(); + // Disable interaction bits + $('.st-interactive').prop('disabled', true); + $('#update-st-div').hide(); + // Force the first check to happen now + vm.checkJob(); + // Set the interval for further checking - this jobs tend to be way faster + // hence setting the interval every 2 seconds. + vm.interval = setInterval(vm.checkJob, 2000); + }, + + /** + * + * Stops the interval checking the status of the job + * + **/ + stopJobCheckInterval: function() { + let vm = this; + clearInterval(vm.interval); + }, + + /** + * + * Performs a call to the server API to create a new sample template + * + **/ + createSampleTemplate: function() { + let vm = this; + var fp = $('#file-select').val(); + var dtype = $('#data-type-select').val(); + var file = $('#st-direct-upload')[0].files[0]; + + var fd = new FormData(); + fd.append('study_id', vm.studyId); + fd.append('filepath', fp); + fd.append('data_type', dtype); + + if (file !== undefined) { + fd.append('direct_upload', true); + fd.append('theFile', file); + } + + $.ajax({ + url: vm.portal + '/study/description/sample_template/', + type: 'POST', + processData: false, + contentType: false, + data: fd, + success: function(data) { + vm.startJobCheckInterval(data['job']); + }, + error: function (object, status, error_msg) { + bootstrapAlert("Error updating sample template: " + error_msg, "danger") + } + }); + }, + + /** + * + * Performs a call to the server API to udpate the sample information + * + **/ + updateSampleTemplate: function() { + let vm = this; + var file = $('#st-direct-upload')[0].files[0]; + + var fd = new FormData(); + fd.append('op', 'replace'); + fd.append('path', vm.studyId + '/data/'); + + if (file !== undefined) { + fd.append('direct_upload', true); + fd.append('value', file); + } else { + fd.append('value', $('#file-select').val()); + } + + $.ajax({ + url: vm.portal + '/study/description/sample_template/', + type: 'PATCH', + processData: false, + contentType: false, + data: fd, + success: function(data) { + vm.startJobCheckInterval(data['job']); + }, + error: function (object, status, error_msg) { + bootstrapAlert("Error updating sample template: " + error_msg, "danger") + } + }); + }, + + /** + * + * Performs a call to the server API to delete a column from the sample template + * + * @param category str The category to be removed + * @param rowId int The row number where this category was placed + * + **/ + deleteColumn: function(category, rowId) { + let vm = this; + $.ajax({ + url: vm.portal + '/study/description/sample_template/', + type: 'PATCH', + data: {'op': 'remove', 'path': vm.studyId + '/columns/' + category}, + success: function(data) { + vm.rowId = rowId; + vm.rowType = 'column'; + vm.startJobCheckInterval(data['job']); + }, + error: function (object, status, error_msg) { + bootstrapAlert("Error deleting column: " + error_msg, "danger") + } + }); + }, + + /** + * + * Performs a call to the server API to delete a sample from the sample template + * + * @param sample str The sample to be removed + * @param rowId int The row number where this sample was placed + * + **/ + deleteSamples: function(samples) { + let vm = this; + var total_samples = samples.length; + if (total_samples === 0){ + alert('No samples selected!'); + } else { + if (confirm('Are you sure you want to delete ' + total_samples + ' samples?')) { + $.ajax({ + url: vm.portal + '/study/description/sample_template/', + type: 'PATCH', + data: {'op': 'remove', 'path': vm.studyId + '/samples/' + samples}, + success: function(data) { + vm.rowId = 0; + vm.rowType = 'sample'; + vm.startJobCheckInterval(data['job']); + }, + error: function (object, status, error_msg) { + bootstrapAlert("Error deleting sample: " + error_msg, "danger") + } + }); + } + } + }, + + /** + * + * Performs a call to the server API to delete the sample template + * + **/ + deleteSampleTemplate: function() { + let vm = this; + if(confirm("Are you sure you want to delete the sample information?")) { + vm.refresh = true; + $.ajax({ + url: vm.portal + '/study/description/sample_template/?study_id=' + vm.studyId, + type: 'DELETE', + success: function(data) { + vm.startJobCheckInterval(data['job']); + }, + error: function (object, status, error_msg) { + bootstrapAlert("Error deleting sample information: " + error_msg, "danger") + } + }); + } + }, + + /** + * + * Creates the GUI for the summary table + * + **/ + populateSampleInfoTable: function() { + let vm = this; + // Gathering this information is expensive in some studies. By issuing + // a different AJAX call for it, we can keep showing the rest of the interface + // (and interact with it) without having to wait for this information to + // show up - also the creation of the table occurs now in client side + // rather than in server side. + $.get(vm.portal + '/study/description/sample_template/columns/', {study_id: vm.studyId}, function(data) { + var catValues, $tr, $td, rowIdx, collapsedId, $trVal, $div, $btn; + $div = $('<div>').addClass('panel panel-default').appendTo('#sample-info-tab'); + $('<div>').addClass('panel-heading').appendTo($div).append('Information summary'); + var $mtable = $('<table>').addClass('table').appendTo($div); + var $table = $('<tbody>').appendTo($mtable); + var categories = data['values']; + categories.sort(function(a, b){return a[0].localeCompare(b[0], 'en', {'sensitivity': 'base'});}); + + rowIdx = 0; + for (var cat of categories) { + $tr = $('<tr>').attr('id', 'col-row-' + rowIdx).appendTo($table); + if (vm.editable && vm.userCanEdit) { + $td = $('<td>').appendTo($tr); + $btn = $('<button>').addClass('btn btn-danger st-interactive').appendTo($td).attr('data-column', cat).attr('data-row-id', rowIdx); + $('<span>').addClass('glyphicon glyphicon-trash').appendTo($btn); + $btn.on('click', function () { + if (confirm('Are you sure you want to delete `' + $(this).attr('data-column') + '`?')) { + vm.deleteColumn($(this).attr('data-column'), $(this).attr('data-row-id')); + } + }); + } + rowIdx += 1; + $td = $('<td>').appendTo($tr); + $('<b>').append(cat + ': ').appendTo($td); + $td.append(' '); + collapsedId = cat + 'collapsed'; + fcall = 'get_column_summary(' + vm.studyId + ', "' + vm.portal + '", "' + cat + '", ' + vm.numSamples + ')'; + $bt = $('<button>').addClass('btn btn-default').attr('onclick', fcall).attr('data-toggle', 'collapse').attr('data-target', '.' + collapsedId).append('Values').appendTo($td); + $trVal = $('<tr>').addClass('collapse').addClass(collapsedId).appendTo($table); + $('<td>').attr('colspan', '3').append(' ').appendTo($trVal); + } + + // Scroll to the desired row + if(vm.rowType === 'column' && vm.rowId !== null) { + // taken from: http://stackoverflow.com/a/2906009 + var container = $('html, body'); + var scrollTo = $('#col-row-' + vm.rowId); + container.animate({ + scrollTop: scrollTo.offset().top - container.offset().top + container.scrollTop() + }); + vm.rowId = null; + } + }) + .fail(function(object, status, error_msg) { + bootstrapAlert("Error loading sample information: " + object.statusText, "danger"); + }); + }, + + /** + * + * Creates the GUI for the Sample-Prep table + * + **/ + populateSamplePrepTab: function() { + let vm = this; + show_loading('sample-prep-tab'); + $.get(vm.portal + '/study/description/sample_summary/', {study_id: vm.studyId}, function(data) { + $('#sample-prep-tab').html(data); + }) + .fail(function(object, status, error_msg) { + bootstrapAlert("Error loading sample-prep information: " + object.statusText, "danger"); + }); + }, + + /** + * + * Creates the GUI for the case that a sample template exists + * + **/ + populateExistingSampleTemplate: function() { + let vm = this; + var $btn, $div, $small, $row, $col, $ul, $li, $tab; + + // Clear the contents of the div + $('#sample-template-contents').empty(); + + // Add the buttons next to the title + // Download Sample Information button + $('#current-sample-info').append(vm.current_file.split('_')[1].split('.')[0]); + $('#title-h3').append(' '); + $btn = $('<a>').addClass('btn btn-default').attr('href', vm.portal + '/download/' + vm.downloadId).appendTo('#title-h3'); + $('<span>').addClass('glyphicon glyphicon-download-alt').appendTo($btn); + $btn.append(' Sample Info'); + // Delete button (only if the user can edit) + if (vm.userCanEdit) { + $('#title-h3').append(' '); + $btn = $('<button>').addClass('btn btn-danger st-interactive').on('click', vm.deleteSampleTemplate).appendTo('#title-h3'); + $('<span>').addClass('glyphicon glyphicon-trash').appendTo($btn); + $btn.append(' Delete'); + } + // Show older files button (only if the sample information has older files) + if (vm.oldFiles.length > 0) { + // Add the button + $('#title-h3').append(' '); + $btn = $('<button>').addClass('btn btn-default').attr('data-toggle', 'collapse').attr('data-target', '#st-old-files').appendTo('#title-h3'); + $('<span>').addClass('glyphicon glyphicon-eye-open').appendTo($btn); + $btn.append(' Show old files'); + + // Add the div that hold the old files + $div = $('<div>').attr('id', 'st-old-files').addClass('collapse').appendTo('#sample-template-contents'); + $div.css('padding', '10px 10px 10px 10px').css('border-radius', '10px').css('background', '#EEE'); + $small = $('<small>').appendTo($div).append('<label>Old files</label>'); + for (var oldFile of vm.oldFiles) { + $small.append('<br/>' + oldFile); + } + } + + // Adding the alert for restrictions + if (vm.sample_restrictions !== '') { + $row = $('<div>').addClass('row').appendTo('#sample-template-contents'); + $col = $('<h5>').appendTo($row); + $('<div>').addClass('alert').addClass('alert-warning').append(vm.sample_restrictions).appendTo($row) + } + + // After adding the buttons we can add the two tabs - one holding the Sample Information + // and the other one holding the Sample and preparation summary + $row = $('<div>').addClass('row').appendTo('#sample-template-contents'); + $col = $('<div>').addClass('col-md-12').appendTo($row); + + // The two "pills" + $ul = $('<ul>').addClass('nav nav-pills').appendTo($col); + $li = $('<li>').css('border', '1px solid #428bca').css('border-radius', '5px').appendTo($ul); + if (vm.rowType == 'column') { + $li.addClass('active'); + } + $('<a>').attr('data-toggle', 'tab').attr('href', '#sample-info-tab').appendTo($li).append('Sample Information'); + $li = $('<li>').css('border', '1px solid #428bca').css('border-radius', '5px').appendTo($ul); + if (vm.rowType == 'sample') { + $li.addClass('active'); + } + $('<a>').attr('data-toggle', 'tab').attr('href', '#sample-prep-tab').appendTo($li).append('Sample-Prep Summary'); + + // The two tab divs are contained in a single tab-content div + $div = $('<div>').addClass('tab-content').appendTo($col); + $tab = $('<div>').addClass('tab-pane').attr('id', 'sample-info-tab').appendTo($div); + if (vm.rowType == 'column') { + $tab.addClass('active'); + } + // Add the number of samples + $tab.append('<label>Number of samples:</label> ' + vm.numSamples + '</br>') + // Add the number of columns + $tab.append('<label>Number of columns:</label> ' + vm.numColumns + '</br>') + + if (vm.userCanEdit) { + // Add the select to update the sample information + $row = $('<div>').attr('id', 'update-st-div').addClass('row form-group').appendTo($tab); + $('<label>').addClass('col-sm-2 col-form-label').append('Update sample information:').appendTo($row); + + // Add the direct upload field + $('<label>') + .addClass('btn btn-default') + .append('Direct upload file <small>(< 2MB)</small>') + .appendTo($row) + .append('<input type="file" style="display: none;" id="st-direct-upload">'); + $('#st-direct-upload').on('change', function() { + if (this.files.length != 1) { + alert('You can only upload one file.') + return false; + } + if (this.files[0].size > vm.maxDirectUploadSize) { + alert('You can only upload files smaller than 2MB. For larger files please use the "Upload Files" button on the left.'); + return false; + } + vm.updateSampleTemplate() + }); + + + $col = $('<div>').addClass('col-sm-3').appendTo($row); + $select = $('<select>').attr('id', 'file-select').addClass('form-control').appendTo($col); + $('<option>').attr('value', "").append('Choose file...').appendTo($select); + for (var opt of vm.uploadedFiles) { + $('<option>').attr('value', opt).append(opt).appendTo($select); + } + // Add the button to trigger the update + $col = $('<div>').addClass('col-sm-2').attr('id', 'update-btn-div').appendTo($row).hide(); + $('<button>').addClass('btn btn-success form-control').append('Update').appendTo($col).on('click', vm.updateSampleTemplate); + $('#file-select').on('change', function() { + if (this.value === "") { + $('#update-btn-div').hide() + } else { + $('#update-btn-div').show() + } + }); + } + + // Populate the sample information table + vm.populateSampleInfoTable(); + + // Sample-prep tab + $tab = $('<div>').addClass('tab-pane').attr('id', 'sample-prep-tab').appendTo($div); + if (vm.rowType === 'sample') { + $tab.addClass('active'); + } + vm.populateSamplePrepTab(); + }, + + /** + * + * Creates the GUI to create a new sample template + * + **/ + populateNewSampleTemplateForm: function() { + let vm = this; + var $row, $col, $select; + + // Clear the contents of the div + $('#sample-template-contents').empty(); + + // To avoid code duplication creating the DOM elements, create a list + // with the contents and create the DOM elements in the for loop + var rowContents = [ + // First one contains the uploaded files + {label: 'Select sample information file:', selectId: 'file-select', options: vm.uploadedFiles, placeholder: 'Choose file...'}, + // Second one contains the data types + {label: 'If uploading a QIIME mapping file, select the data type of the prep information:', selectId: 'data-type-select', options: vm.dataTypes, placeholder: 'Choose a data type...'}] + + // Create the DOM elements + for (var rC of rowContents) { + $row = $('<div>').addClass('row form-group').appendTo('#sample-template-contents'); + $('<label>').addClass('col-sm-3 col-form-label').append(rC.label).appendTo($row); + $col = $('<div>').addClass('col-sm-3').appendTo($row); + $select = $('<select>').attr('id', rC.selectId).addClass('form-control').appendTo($col); + $('<option>').attr('value', "").append(rC.placeholder).appendTo($select); + for (var opt of rC.options) { + $('<option>').attr('value', opt).append(opt).appendTo($select); + } + + if (rC['selectId'] == 'file-select') { + // Add the direct upload field + $('<label>') + .addClass('btn btn-default') + .append('Direct upload file <small>(< 2MB)</small>') + .appendTo($row) + .append('<input type="file" style="display: none;" id="st-direct-upload">'); + $('#st-direct-upload').on('change', function() { + if (this.files.length != 1) { + alert('You can only upload one file.') + return false; + } + if (this.files[0].size > vm.maxDirectUploadSize) { + alert('You can only upload files smaller than 2MB. For larger files please use the "Upload Files" button on the left.'); + return false; + } + vm.createSampleTemplate() + }); + } + } + + // Add the button - by default hidden + $row = $('<div>').attr('id', 'create-btn-div').addClass('row form-group').appendTo('#sample-template-contents').hide(); + $col = $('<div>').addClass('col-sm-1').appendTo($row); + $('<button>').addClass('btn btn-success form-control st-interactive').append('Create').appendTo($col).on('click', vm.createSampleTemplate); + + // Show/hide the button base on the value of the file selector + $('#file-select').on('change', function() { + if (this.value === "") { + $('#create-btn-div').hide() + } else { + $('#create-btn-div').show() + } + }); + }, + + /** + * + * Performs a query to the server to update the sample template overview information + * + **/ + updateSampleTemplateOverview: function () { + let vm = this; + + $.get(vm.portal + '/study/description/sample_template/overview/', {study_id: vm.studyId}, function(data) { + vm.exists = data['exists']; + vm.dataTypes = data['data_types']; + vm.uploadedFiles = data['uploaded_files']; + vm.userCanEdit = data['user_can_edit']; + vm.job = data['job']; + vm.downloadId = data['download_id']; + vm.current_file = data['st_files'][0]; + vm.oldFiles = data['st_files'].slice(1); + vm.numSamples = data['num_samples']; + vm.numColumns = data['num_columns']; + vm.columns = data['columns']; + vm.sample_restrictions = data['sample_restrictions']; + + // fixing message for nicer display + if (vm.sample_restrictions !== '') { + vm.sample_restrictions = "Sample Info " + vm.sample_restrictions.split(" ").slice(2).join(" "); + } + + // Populate the sample-template-contents + $('#title-h3').empty(); + if (!vm.exists) { + vm.populateNewSampleTemplateForm(); + } else { + vm.populateExistingSampleTemplate(); + } + + // Check the job for first time + if (vm.job !== null) { + $.get(vm.portal + '/study/process/job/', {job_id: vm.job}, function(data) { + var jobStatus = data['job_status']; + if (jobStatus === 'error') { + bootstrapAlert(data['job_error'], "danger"); + } else if (jobStatus !== 'success') { + vm.startJobCheckInterval(vm.job); + } + }) + .fail(function(object, status, error_msg) { + bootstrapAlert("Error checking the job status: " + object.statusText, "danger"); + }); + } + + }) + .fail(function(object, status, error_msg) { + bootstrapAlert("Error gathering Sample Information from server: " + object.statusText, "danger"); + }); + }, + + /** + * + * Cleans up the current object + * + **/ + destroy: function() { + let vm = this; + vm.stopJobCheckInterval() + } + }, + /** + * + * This function gets called by Vue once the HTML template is ready in the DOM + * + **/ + mounted() { + let vm = this; + + vm.interval = null; + vm.job = null; + vm.editable = true; + vm.rowId = null; + vm.rowType = 'column'; + vm.refresh = false; + $('#st-processsing-div').hide(); + + show_loading('sample-template-contents'); + + // Get the overview information from the server + vm.updateSampleTemplateOverview(); + } +}); + +/** + * + * Creates a new Vue object for the Sample Template in a safe way + * + * @param target str The id of the target div for the new Vue object + * + **/ +function newSampleTemplateVue(target) { + if (sampleTemplatePage !== null) { + sampleTemplatePage.$refs.stElem.destroy(); + } + sampleTemplatePage = new Vue({el: target}); +};