diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 43256d1e44..407c507cc5 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -3,6 +3,10 @@ The Narrative Interface allows users to craft KBase Narratives using a combinati This is built on the Jupyter Notebook v6.0.2 (more notes will follow). +### Version 4.1.1 +- Fix sort order in Narratives panel - should be by most recently saved. +- Add better error support in data staging uploader - if an upload directory is not available, you should be able to return to the root directory without trouble. + ### Version 4.1.0 - Introduce Static Narratives, available under the Share menu. First release! diff --git a/bower.json b/bower.json index e719928853..39211a66aa 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "kbase-narrative", - "version": "4.1.0", + "version": "4.1.1", "homepage": "https://kbase.us", "dependencies": { "bluebird": "3.4.7", diff --git a/kbase-extension/static/kbase/css/kbaseJobLog.css b/kbase-extension/static/kbase/css/kbaseJobLog.css index a3dc1627b5..3da3e65501 100644 --- a/kbase-extension/static/kbase/css/kbaseJobLog.css +++ b/kbase-extension/static/kbase/css/kbaseJobLog.css @@ -16,13 +16,11 @@ .kblog-header { display: flex; font-family: monospace; - font-size: 85%; + font-size: 85%; } .kblog-line { - display: flex; font-family: monospace; - font-size: 85%; } .kblog-line:hover { @@ -31,11 +29,13 @@ .kblog-num-wrapper { font-size: 85%; + display: flex; + flex-direction: row; } .kblog-line-num { flex-shrink: 0; - width: 9ex; + width: 3rem; text-align: right; color: #999; white-space: nowrap; @@ -72,4 +72,4 @@ #kblog-err { margin-top: 5px; /* color: #660000; */ -} \ No newline at end of file +} diff --git a/kbase-extension/static/kbase/js/util/jobLogViewer.js b/kbase-extension/static/kbase/js/util/jobLogViewer.js index 2958ab1ca4..9888d91472 100644 --- a/kbase-extension/static/kbase/js/util/jobLogViewer.js +++ b/kbase-extension/static/kbase/js/util/jobLogViewer.js @@ -1,5 +1,13 @@ /*global define*/ /*jslint white:true,browser:true*/ +/** + * Usage: + * let viewer = JobLogViewer.make(); + * viewer.start({ + * jobId: , + * node: + * }) + */ define([ 'bluebird', 'common/runtime', @@ -8,7 +16,6 @@ define([ 'common/events', 'common/fsm', 'kb_common/html', - 'jquery', 'css!kbase/css/kbaseJobLog.css' ], function( Promise, @@ -17,22 +24,21 @@ define([ UI, Events, Fsm, - html, - $ + html ) { 'use strict'; - var t = html.tag, + let t = html.tag, div = t('div'), button = t('button'), span = t('span'), p = t('p'), fsm, currentSection, - renderAbove = true, smallPanelHeight = '300px', largePanelHeight = '600px', numLines = 10, + panel, panelHeight = smallPanelHeight, appStates = [{ state: { @@ -44,7 +50,7 @@ define([ ui: { buttons: { enabled: [], - disabled: ['play', 'stop', 'top', 'back', 'forward', 'bottom'] + disabled: ['play', 'stop', 'top', 'bottom', 'expand'] } }, next: [{ @@ -81,7 +87,7 @@ define([ ui: { buttons: { enabled: [], - disabled: ['play', 'stop', 'top', 'back', 'forward', 'bottom'] + disabled: ['play', 'stop', 'top', 'bottom', 'expand'] } }, next: [{ @@ -130,8 +136,8 @@ define([ }, ui: { buttons: { - enabled: ['stop'], - disabled: ['play', 'top', 'back', 'forward', 'bottom'] + enabled: ['stop', 'expand'], + disabled: ['play', 'top', 'bottom'] } }, next: [{ @@ -180,7 +186,7 @@ define([ }, ui: { buttons: { - enabled: ['play', 'top', 'back', 'forward', 'bottom'], + enabled: ['play', 'top', 'bottom', 'expand'], disabled: ['stop'] } }, @@ -223,7 +229,7 @@ define([ }, ui: { buttons: { - enabled: ['top', 'back', 'forward', 'bottom'], + enabled: ['top', 'bottom', 'expand'], disabled: ['play', 'stop'] } }, @@ -251,7 +257,7 @@ define([ }, ui: { buttons: { - enabled: ['top', 'back', 'forward', 'bottom'], + enabled: ['top', 'bottom', 'expand'], disabled: ['play', 'stop'] } }, @@ -279,7 +285,7 @@ define([ }, ui: { buttons: { - enabled: ['top', 'back', 'forward', 'bottom'], + enabled: ['top', 'bottom', 'expand'], disabled: ['play', 'stop'] } }, @@ -311,7 +317,7 @@ define([ ui: { buttons: { enabled: [], - disabled: ['play', 'stop', 'top', 'back', 'forward', 'bottom'] + disabled: ['play', 'stop', 'top', 'bottom', 'expand'] } }, on: { @@ -332,15 +338,21 @@ define([ } ]; - function factory(config) { - var config = config || {}, - runtime = Runtime.make(), + /** + * The entrypoint to this widget. This creates the job log viewer and initializes it. + * Starting it is left as a lifecycle method for the calling object. + * + */ + function factory() { + let runtime = Runtime.make(), bus = runtime.bus().makeChannelBus({ description: 'Log Viewer Bus' }), container, jobId, + panelId, model, ui, - linesPerPage = config.linesPerPage || numLines, + startingLine = 0, + linesPerPage = null, loopFrequency = 5000, looping = false, stopped = false, @@ -413,6 +425,13 @@ define([ } function requestLatestJobLog() { + // only while job is running + // load numLines at a time + // otherwise load entire log + let autoState = fsm.getCurrentState().state.auto; + if(autoState){ + linesPerPage = numLines; // set it to 10 + } ui.showElement('spinner'); runtime.bus().emit('request-latest-job-log', { jobId: jobId, @@ -421,110 +440,42 @@ define([ } }); } - function fetchNewLogs(currentLine) { - var $panel = $(ui.getElements('panel')[0]), - first = Number($panel.children().first().attr('class')); - if (currentLine === 0 && first === 0) { - return; - } - var newFirstLine = currentLine - Number(linesPerPage), - numLines = linesPerPage; - - if (newFirstLine < 0) { - newFirstLine = 0; - numLines = currentLine; - } - requestJobLog(newFirstLine, Number(numLines)); - currentSection = newFirstLine; - } - - function scrollToLog($panel, target, scrollTime){ - if(target.length){ - scrollTime = (scrollTime !== undefined) ? scrollTime : 500; - $panel.animate({ - scrollTop: target.offset().top - ($panel.offset().top - $panel.scrollTop()) - }, scrollTime, function () { - currentSection = target.parent().attr('class'); - }); - }else{ - fetchNewLogs(Number(model.getItem('currentLine'))); - } - - } + /** + * Scroll to the top of the job log + */ function doFetchFirstLogChunk() { doStopPlayLogs(); - renderAbove = true; - - var currentLine = currentSection ? currentSection : Number(model.getItem('currentLine')), - $currentSection = $('.' + String(currentLine)); - - if ($currentSection.is(':first-child')) { - fetchNewLogs(currentLine); - } else { - var $panel = $(ui.getElements('panel')[0]), - target = $panel.children().first().children().first(); - - scrollToLog($panel, target); - - } - } - - - function doFetchPreviousLogChunk() { - doStopPlayLogs(); - renderAbove = true; - - var currentLine = currentSection ? currentSection : Number(model.getItem('currentLine')), - $currentSection = $('.' + String(currentLine)); - if (!$currentSection.is(':first-child')) { - var $panel = $(ui.getElements('panel')[0]), - target = $currentSection.prev().children().first(); - scrollToLog($panel, target); - } else { - fetchNewLogs(currentLine); - } - - } - - function doFetchNextLogChunk() { - renderAbove = false; - - doStopPlayLogs(); - var currentLine = currentSection ? currentSection : Number(model.getItem('currentLine')), - lastLine = model.getItem('lastLine'), - $panel = $(ui.getElements('panel')[0]); - - var $currentSection = $('.' + String(currentLine)); - currentSection = currentLine; - - if ($currentSection.is(':last-child')) { - requestJobLog(lastLine); - } else { - var target = $currentSection.next().children().last(); - scrollToLog($panel, target); - } + panel.scrollTo({ + top: 0, + left: 0, + behavior: 'smooth' + }) } + /** + * scroll to the bottom of the job log + */ function doFetchLastLogChunk() { - renderAbove = false; - doStopPlayLogs(); - var $panel = $(ui.getElements('panel')[0]); - var target = $panel.children().last().children().last(); - scrollToLog($panel, target); + const lastChildElement = panel.lastChild + lastChildElement.scrollIntoView({ + alignToTop: false, + behavior: 'smooth', + block: 'center' + }) } - function test(){ - if(panelHeight === smallPanelHeight){ - panelHeight = largePanelHeight; - }else{ - panelHeight = smallPanelHeight; - } - $(ui.getElements('panel')[0]).animate({height: panelHeight}, 500); + + function toggleViewerSize() { + panelHeight = panelHeight === smallPanelHeight ? largePanelHeight : smallPanelHeight; + getPanelNode().style.height = panelHeight; } // VIEW - + /** + * builds contents of panel-heading div + * @param {??} events + */ function renderControls(events) { return div({ dataElement: 'header', style: { margin: '0 0 10px 0' } }, [ button({ @@ -532,10 +483,10 @@ define([ dataButton: 'expand', dataToggle: 'tooltip', dataPlacement: 'top', - title: 'Start fetching logs', + title: 'Toggle log viewer size', id: events.addEvent({ type: 'click', - handler: test + handler: toggleViewerSize }) }, [ span({ class: 'fa fa-expand' }) @@ -579,32 +530,6 @@ define([ }, [ span({ class: 'fa fa-angle-double-up' }) ]), - button({ - class: 'btn btn-sm btn-default', - dataButton: 'back', - dataToggle: 'tooltip', - dataPlacement: 'top', - title: 'Fetch previous log chunk', - id: events.addEvent({ - type: 'click', - handler: doFetchPreviousLogChunk - }) - }, [ - span({ class: 'fa fa-angle-up' }) - ]), - button({ - class: 'btn btn-sm btn-default', - dataButton: 'forward', - dataToggle: 'tooltip', - dataPlacement: 'top', - title: 'Fetch next log chunk', - id: events.addEvent({ - type: 'click', - handler: doFetchNextLogChunk - }) - }, [ - span({ class: 'fa fa-angle-down' }) - ]), button({ class: 'btn btn-sm btn-default', dataButton: 'bottom', @@ -625,21 +550,28 @@ define([ ]); } - function renderLayout() { - var events = Events.make(), + /** + * builds contents of panel-body class + * @param {string} panelId + */ + function renderLayout(panelId) { + const events = Events.make(), content = div({ dataElement: 'kb-log', style: { marginTop: '10px'}}, [ div({ class: 'kblog-header' }, [ div({ class: 'kblog-num-wrapper' }, [ div({ class: 'kblog-line-num' }, []) ]), div({ class: 'kblog-text' }, [ - renderControls(events) + renderControls(events) // header ]) ]), - div({ dataElement: 'panel', + div({ dataElement: 'panel', id: panelId, style: { - 'overflow-y': 'scroll', height: panelHeight - } }) + 'overflow-y': 'scroll', + height: panelHeight, + transition: 'height 0.5s' + } + }) ]); return { @@ -665,88 +597,87 @@ define([ return fixed.join(' '); } - //left in because oher methods depend on it - function renderLine(line) { - var extraClass = line.isError ? ' kb-error' : ''; - - return div({ - class: 'kblog-line' + extraClass - }, [ - div({ class: 'kblog-num-wrapper' }, [ - div({ class: 'kblog-line-num' }, [ - String(line.lineNumber) - ]) - ]), - div({ - class: 'kblog-text', - style: { - overflow: 'auto' - } - }, [ - div({ style: { marginBottom: '6px' } }, sanitize(line.text)) - ]) - ]); - } - function renderLine2(line) { - var extraClass = line.isError ? ' kb-error' : ''; - var $line = $('
') - .addClass('kblog-num-wrapper' ) - .append($('') - .addClass('kblog-line-num') - .append(String(line.lineNumber))) - .append($('') - .addClass('kblog-text') - .append(sanitize(line.text))); - return $('
') - .addClass('kblog-line' + extraClass) - .append($line); + /** + * build and return div that displays + * individual job log line + *
+ *
+ * ### + * foobarbaz + *
+ *
+ * @param {object} line + */ + function buildLine(line) { + // kblog-line wrapper div + const errorClass = line.isError ? ' kb-error' : ''; + const kblogLine = document.createElement('div') + kblogLine.setAttribute('class', 'kblog-line' + errorClass); + // kblog-num-wrapper div + const wrapperDiv = document.createElement('div'); + wrapperDiv.setAttribute('class', 'kblog-num-wrapper'); + // number + const numDiv = document.createElement('div'); + numDiv.setAttribute('class', 'kblog-line-num'); + const lineNumber = document.createTextNode(line.lineNumber); + numDiv.appendChild(lineNumber); + // text + const textDiv = document.createElement('div'); + textDiv.setAttribute('class', 'kblog-text'); + const lineText = document.createTextNode(line.text) + textDiv.appendChild(lineText); + // append line number and text + wrapperDiv.appendChild(numDiv); + wrapperDiv.appendChild(textDiv); + // append wrapper to line div + kblogLine.appendChild(wrapperDiv); + + return kblogLine; } - function renderLines(lines) { - var $section = $('
'); - for(var i = lines.length-1; i>=0; i--){ - $section.prepend(renderLine2(lines[i])); + /** + * Append div that displays job log lines + * to the panel + * @param {array} lines + */ + function makeLogChunkDiv(lines) { + for (let i=0; i b.nar[3]) - return -1; // sort by date - if (a.nar[3] < b[3]) - return 1; // sort by date - return 0; + return b.nar[3].localeCompare(a.nar[3]); }, renderPanel: function () { diff --git a/kbase-extension/static/kbase/js/widgets/narrative_core/upload/fileUploadWidget.js b/kbase-extension/static/kbase/js/widgets/narrative_core/upload/fileUploadWidget.js index 56fe0c191c..0df24fef1a 100644 --- a/kbase-extension/static/kbase/js/widgets/narrative_core/upload/fileUploadWidget.js +++ b/kbase-extension/static/kbase/js/widgets/narrative_core/upload/fileUploadWidget.js @@ -36,21 +36,17 @@ define([ render: function() { var $dropzoneElem = $(this.dropzoneTmpl({userInfo: this.userInfo})); - $dropzoneElem.find('#clear-completed > button').click(function(e) { + $dropzoneElem.find('#clear-completed > button').click(e => { e.preventDefault(); e.stopPropagation(); this.dropzone.removeAllFiles(); $dropzoneElem.find('#clear-completed').css({'display': 'none'}); - }.bind(this)); + }); - // $dropzoneElem.find('a').click((e) => { - // e.stopPropagation(); - // }); - // there are two anchor elements with same class name .globus_link. - // One link takes the user to globus site, - // and the other link takes user to how to link globus account. - $dropzoneElem.find('a.globus_link').click((e) => { + // One link takes the user to globus site, + // and the other link takes user to how to link globus account. + $dropzoneElem.find('a.globus_link').click(e => { e.stopPropagation(); e.preventDefault(); if((e.target.href).includes("app.globus.org")) { @@ -83,14 +79,14 @@ define([ parallelUploads: 10, maxFilesize: 20480 //20GB }) - .on('totaluploadprogress', function(progress) { + .on('totaluploadprogress', (progress) => { $($dropzoneElem.find('#total-progress .progress-bar')).css({'width': progress + '%'}); - }.bind(this)) - .on('addedFile', function(file) { + }) + .on('addedFile', (file) => { $dropzoneElem.find('#global-info').css({'display': 'inline'}); $dropzoneElem.find('#upload-message').text(this.makeUploadMessage()); - }.bind(this)) - .on('success', function(file, serverResponse) { + }) + .on('success', (file, serverResponse) => { $dropzoneElem.find('#clear-completed').css({'display': 'inline'}); $dropzoneElem.find('#upload-message').text(this.makeUploadMessage()); file.previewElement.querySelector('#status-message').textContent = 'Completed'; @@ -107,9 +103,8 @@ define([ $(file.previewElement).fadeOut(1000, function() { $(file.previewElement.querySelector('.btn')).trigger('click'); }); - }.bind(this)) - .on('sending', function(file, xhr, data) { - + }) + .on('sending', (file, xhr, data) => { $dropzoneElem.find('#global-info').css({'display': 'inline'}); //okay, if we've been given a full path, then we pull out the pieces (ignoring the filename at the end) and then //tack it onto our set path, then set that as the destPath form param. @@ -123,10 +118,17 @@ define([ } $($dropzoneElem.find('#total-progress')).show(); $dropzoneElem.find('#upload-message').text(this.makeUploadMessage()); - }.bind(this)) + }) .on('reset', function() { $dropzoneElem.find('#global-info').css({'display': 'none'}); $($dropzoneElem.find('#total-progress .progress-bar')).css({'width': '0%'}); + }) + .on('error', (err) => { + let errorText = 'unable to upload file!'; + if (err && err.xhr && err.xhr.responseText) { + errorText = err.xhr.responseText; + } + $dropzoneElem.find('.error.text-danger').text('Error: ' + errorText); }); }, diff --git a/kbase-extension/static/kbase/js/widgets/narrative_core/upload/stagingAreaViewer.js b/kbase-extension/static/kbase/js/widgets/narrative_core/upload/stagingAreaViewer.js index bed4337a62..b7495fedac 100644 --- a/kbase-extension/static/kbase/js/widgets/narrative_core/upload/stagingAreaViewer.js +++ b/kbase-extension/static/kbase/js/widgets/narrative_core/upload/stagingAreaViewer.js @@ -76,9 +76,9 @@ define([ activate: function () { this.render(); if (!this.refreshInterval) { - this.refreshInterval = setInterval(function () { + this.refreshInterval = setInterval(() => { this.render(); - }.bind(this), this.options.refreshIntervalDuration); + }, this.options.refreshIntervalDuration); } }, @@ -89,28 +89,29 @@ define([ } }, + /** + * Returns a Promise that resolves once the rendering is done. + */ render: function () { - this.updateView(); + return this.updateView(); }, updateView: function () { - return this.stagingServiceClient.list({ + return Promise.resolve(this.stagingServiceClient.list({ path: this.subpath - }) - .then(function (data) { + })) + .then(data => { //list is recursive, so it'd show all files in all subdirectories. This filters 'em out. - var files = JSON.parse(data).filter(function (f) { - // this is less complicated than you think. The path is the username, subpath, and name concatenated. The subpath may be empty - // so we filter it out and only join defined things. If that's the same as the file's path, we're at the right level. If not, we're not. - if ([this.userInfo.user, this.subpath, f.name].filter(function (p) { - return p.length > 0; - }).join('/') === f.path) { - return true; - } else { - return false; - } - }.bind(this)); - files.forEach(function (f) { + let files = JSON.parse(data).filter(f => { + // this is less complicated than you think. The path is the username, + // subpath, and name concatenated. The subpath may be empty so we + // filter it out and only join defined things. If that's the same as + // the file's path, we're at the right level. If not, we're not. + return [this.userInfo.user, this.subpath, f.name] + .filter(p => p.length > 0) + .join('/') === f.path; + }); + files.forEach(f => { if (!f.isFolder) { f.imported = {}; } @@ -120,18 +121,18 @@ define([ this.$elem.empty(); this.renderFileHeader(); this.renderFiles(files); - setTimeout(function () { + setTimeout(() => { this.$elem.parent().scrollTop(scrollTop) - }.bind(this), 0); - }.bind(this)) - .fail(function (xhr) { + }, 0); + }) + .catch(xhr => { this.$elem.empty(); - this.$elem.append( - $.jqElem('div') - .addClass('alert alert-danger') - .append('Error ' + xhr.status + '
' + xhr.responseText) - ); - }.bind(this)); + this.renderFileHeader(); + this.renderError(xhr.responseText ? xhr.responseText : 'Unknown error - directory was not found, or may have been deleted'); + }) + .finally(() => { + this.renderPath(); + }); }, /** @@ -147,7 +148,7 @@ define([ subpathTokens--; } this.subpath = subpath.slice(subpath.length - subpathTokens).join('/'); - this.updateView(); + return this.updateView(); }, renderFileHeader: function () { @@ -155,9 +156,9 @@ define([ // Set up the link to the web upload app. - this.$elem.find('.web_upload_div').click(function () { + this.$elem.find('.web_upload_div').click(() => { this.initImportApp('web_upload'); - }.bind(this)); + }); // Add ACL before going to the staging area // If it fails, it'll just do so silently. @@ -175,9 +176,9 @@ define([ }); // Bind the help button to start the tour. - this.$elem.find('button#help').click(function () { + this.$elem.find('button#help').click(() => { this.startTour(); - }.bind(this)); + }); }, renderPath: function () { @@ -186,9 +187,7 @@ define([ splitPath = splitPath.substring(1); } // the staging service doesn't want the username as part of the path, but we still want to display it to the user for navigation purposes - splitPath = splitPath.split('/').filter(function (p) { - return p.length - }); + splitPath = splitPath.split('/').filter(p => p.length); splitPath.unshift(this.userInfo.user); var pathTerms = []; for (var i = 0; i < splitPath.length; i++) { @@ -206,37 +205,54 @@ define([ this.$elem.find('div.file-path').append(this.filePathTmpl({ path: pathTerms })); - this.$elem.find('div.file-path a').click(function (e) { + this.$elem.find('div.file-path a').click(e => { this.updatePathFn($(e.currentTarget).data().element); - }.bind(this)); - this.$elem.find('button#refresh').click(function () { + }); + this.$elem.find('button#refresh').click(() => { this.updateView(); - }.bind(this)); + }); }, downloadFile: function(url) { - console.log("Downloading url=" + url); - const hiddenIFrameID = 'hiddenDownloader'; + const hiddenIFrameID = 'hiddenDownloader'; let iframe = document.getElementById(hiddenIFrameID); - if (iframe === null) { - iframe = document.createElement('iframe'); - iframe.id = hiddenIFrameID; - iframe.style.display = 'none'; - document.body.appendChild(iframe); - } - iframe.src = url; + if (iframe === null) { + iframe = document.createElement('iframe'); + iframe.id = hiddenIFrameID; + iframe.style.display = 'none'; + document.body.appendChild(iframe); + } + iframe.src = url; }, - renderFiles: function (files) { - - var parent = this.$elem.parent().get(0); + renderError: function (error) { + const errorElem = ` +
+
+ An error occurred while fetching your files: ${error} +
+ `; + this.$elem.append(errorElem); + }, + /** + * This renders the files datatable. If there's no data, it gives a message + * about no files being present. If there's an error, that gets put in the table instead. + * @param {object} data + * keys: files (list of file info) and error (optional error) + */ + renderFiles: function (files) { + files = files || []; + const emptyMsg = 'No files found.'; var $fileTable = $(this.ftpFileTableTmpl({ files: files, uploaders: this.uploaders.dropdown_order })); this.$elem.append($fileTable); this.$elem.find('table').dataTable({ + language: { + emptyTable: emptyMsg + }, dom: '<"file-path pull-left">frtip', bAutoWidth: false, aaSorting: [ @@ -316,14 +332,14 @@ define([ $('td:eq(4)', nRow).find('select').select2({ placeholder: 'Select format' }); - $('td:eq(4)', nRow).find('button[data-import]').off('click').on('click', function (e) { + $('td:eq(4)', nRow).find('button[data-import]').off('click').on('click', e => { var importType = $(e.currentTarget).prevAll('#import-type').val(); var importFile = getFileFromName($(e.currentTarget).data().import); this.initImportApp(importType, importFile); this.updateView(); - }.bind(this)); + }); - $('td:eq(4)', nRow).find('button[data-download]').off('click').on('click', (e) => { + $('td:eq(4)', nRow).find('button[data-download]').off('click').on('click', e => { let file = $(e.currentTarget).data('download'); if (this.subpath) { file = this.subpath + '/' + file; @@ -332,24 +348,23 @@ define([ this.downloadFile(url); }); - $('td:eq(4)', nRow).find('button[data-delete]').off('click').on('click', function (e) { + $('td:eq(4)', nRow).find('button[data-delete]').off('click').on('click', e => { var file = $(e.currentTarget).data('delete'); if (window.confirm('Really delete ' + file + '?')) { this.stagingServiceClient.delete({ path: this.subpath + '/' + file - }).then(function (d, s, x) { + }).then(() => { this.updateView(); - }.bind(this)) - .fail(function (xhr) { - alert('Error ' + xhr.status + '\r' + xhr.responseText); - }.bind(this)); + }).fail(xhr => { + alert('Error ' + xhr.status + '\r' + xhr.responseText); + }); } - }.bind(this)); + }); - $('td:eq(0)', nRow).find('button[data-name]').off('click').on('click', function (e) { + $('td:eq(0)', nRow).find('button[data-name]').off('click').on('click', e => { this.updatePathFn(this.path += '/' + $(e.currentTarget).data().name); - }.bind(this)); + }); $('td:eq(0)', nRow).find('i[data-caret]').off('click'); @@ -371,15 +386,15 @@ define([ //toggle the caret $caret.toggleClass('fa-caret-down fa-caret-right'); //and append the detailed view, which we do in a timeout in the next pass through to ensure that everything is properly here. - setTimeout(function () { + setTimeout(() => { $caret.parent().parent().after( this.renderMoreFileInfo(myFile) ) - }.bind(this), 0); + }, 0); } - $('td:eq(0)', nRow).find('i[data-caret]').on('click', function (e) { + $('td:eq(0)', nRow).find('i[data-caret]').on('click', e => { $(e.currentTarget).toggleClass('fa-caret-down fa-caret-right'); var $tr = $(e.currentTarget).parent().parent(); @@ -397,10 +412,10 @@ define([ $tr.next().detach(); delete this.openFileInfo[fileName]; } - }.bind(this)); + }); $('td:eq(1)', nRow).find('button[data-decompress]').off('click'); - $('td:eq(1)', nRow).find('button[data-decompress]').on('click', function (e) { + $('td:eq(1)', nRow).find('button[data-decompress]').on('click', e => { var fileName = $(e.currentTarget).data().decompress; var myFile = getFileFromName(fileName); @@ -409,18 +424,15 @@ define([ this.stagingServiceClient.decompress({ path: myFile.name }) - .then(function () { - this.updateView(); - }.bind(this)) - .fail(function (xhr) { - console.log("FAILED", xhr); + .then(() => this.updateView()) + .fail(xhr => { + console.error("FAILED", xhr); alert(xhr.responseText); - }.bind(this)); + }); - }.bind(this)); + }); }.bind(this) }); - this.renderPath(); }, renderMoreFileInfo: function (fileData) { @@ -546,7 +558,6 @@ define([ }) .fail(function (xhr) { - console.log("FAILED TO LOAD METADATA : ", fileData, xhr); $tabsDiv.empty(); $tabsDiv.append( $.jqElem('div') @@ -612,4 +623,4 @@ define([ this.tour.start(); } }); -}); \ No newline at end of file +}); diff --git a/package-lock.json b/package-lock.json index 9447442b5e..59f84fe34f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "kbase-narrative-core", - "version": "4.0.0", + "version": "4.1.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 3825435a27..7a632765dc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "kbase-narrative-core", "description": "Core components for the KBase Narrative Interface", - "version": "4.1.0", + "version": "4.1.1", "private": true, "repository": "github.com/kbase/narrative", "devDependencies": { diff --git a/src/biokbase/narrative/__init__.py b/src/biokbase/narrative/__init__.py index f5eb64eaaa..cbca9fea2c 100644 --- a/src/biokbase/narrative/__init__.py +++ b/src/biokbase/narrative/__init__.py @@ -1,7 +1,7 @@ __all__ = ['magics', 'common', 'handlers', 'contents', 'services', 'widgetmanager'] from semantic_version import Version -__version__ = Version("4.1.0") +__version__ = Version("4.1.1") version = lambda: __version__ # if run directly: diff --git a/src/config.json b/src/config.json index a3bd1c8f88..deb379b2e3 100644 --- a/src/config.json +++ b/src/config.json @@ -262,5 +262,5 @@ "showDelay": 750 }, "use_local_widgets": true, - "version": "4.1.0" + "version": "4.1.1" } diff --git a/src/config.json.templ b/src/config.json.templ index 308087a987..d322bde4c8 100644 --- a/src/config.json.templ +++ b/src/config.json.templ @@ -321,5 +321,5 @@ "showDelay": 750 }, "use_local_widgets": true, - "version": "4.1.0" + "version": "4.1.1" } diff --git a/src/requirements.txt b/src/requirements.txt index d0e03ca785..ff1ecbaced 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -5,7 +5,7 @@ rsa==4.0 pyasn1==0.4.5 requests==2.22.0 pyyaml==5.2 -jinja2==2.10 +jinja2==2.11.1 sympy==1.3 setuptools==40.8.0 semantic_version==2.6.0 diff --git a/test/unit/spec/Util/jobLogViewerSpec.js b/test/unit/spec/Util/jobLogViewerSpec.js new file mode 100644 index 0000000000..8b156455d6 --- /dev/null +++ b/test/unit/spec/Util/jobLogViewerSpec.js @@ -0,0 +1,129 @@ +/*global define, describe, it, expect, jasmine, beforeEach, afterEach*/ +/*jslint white: true*/ +define([ + 'util/jobLogViewer', + 'common/runtime' +], ( + JobLogViewer, + Runtime +) => { + describe('Test the job log viewer module', () => { + let hostNode = null, + runtimeBus = null; + beforeEach(() => { + hostNode = document.createElement('div'); + document.body.appendChild(hostNode); + runtimeBus = Runtime.make().bus(); + }); + + afterEach(() => { + hostNode.remove(); + window.kbaseRuntime = null; + }); + + it('Should load the module code successfully', () => { + expect(JobLogViewer).toBeDefined(); + }); + + it('Should have the factory method', () => { + expect(JobLogViewer.make).toBeDefined(); + expect(JobLogViewer.make).toEqual(jasmine.any(Function)); + }); + + it('Should be created', () => { + let viewer = JobLogViewer.make(); + expect(viewer.start).toEqual(jasmine.any(Function)); + expect(viewer.stop).toEqual(jasmine.any(Function)); + expect(viewer.detach).toEqual(jasmine.any(Function)); + }); + + it('Should fail to start without a node', () => { + let viewer = JobLogViewer.make(); + const jobId = 'fakejob'; + let arg = { + jobId: jobId + }; + expect(() => {viewer.start(arg)}).toThrow(new Error('Requires a node to start')); + }); + + it('Should fail to start without a jobId', () => { + let viewer = JobLogViewer.make(); + let arg = { + node: hostNode + }; + expect(() => {viewer.start(arg)}).toThrow(new Error('Requires a job id to start')); + }); + + it('Should start as expected with inputs, and be stoppable and detachable', () => { + let viewer = JobLogViewer.make(); + let arg = { + node: hostNode, + jobId: 'someFakeJob' + }; + viewer.start(arg); + expect(hostNode.querySelector('div[data-element="kb-log"]')).toBeDefined(); + viewer.detach(); + expect(hostNode.innerHTML).toBe(''); + }); + + it('Should send a bus messages requesting job status information at startup', (done) => { + let viewer = JobLogViewer.make(); + const jobId = 'testJob1'; + const arg = { + node: hostNode, + jobId: jobId + }; + runtimeBus.on('request-job-status', (msg) => { + expect(msg).toEqual({jobId: jobId}); + viewer.detach(); + done(); + }); + viewer.start(arg); + }); + + it('Should react to job status messages', (done) => { + let viewer = JobLogViewer.make(); + const jobId = 'testJobStatusMsg'; + const arg = { + node: hostNode, + jobId: jobId + }; + runtimeBus.on('request-job-status', (msg) => { + expect(msg).toEqual({jobId: jobId}); + runtimeBus.send( + { + jobId: jobId, + jobState: { + job_state: 'in-progress' + } + }, + { + channel: { + jobId: jobId + }, + key: { + type: 'job-status' + } + } + ); + viewer.detach(); + done(); + }); + viewer.start(arg); + }); + + it('Should start with all buttons disabled', () => { + let viewer = JobLogViewer.make(); + const jobId = 'testBtnState'; + const arg = { + node: hostNode, + jobId: jobId + }; + viewer.start(arg); + let btns = hostNode.querySelectorAll('div[data-element="header"] button'); + btns.forEach(btn => { + expect(btn.classList.contains('disabled')).toBeTruthy(); + }); + }); + }); +}) diff --git a/test/unit/spec/narrative_core/upload/stagingAreaViewer-spec.js b/test/unit/spec/narrative_core/upload/stagingAreaViewer-spec.js index 78d608b058..6a4e061520 100644 --- a/test/unit/spec/narrative_core/upload/stagingAreaViewer-spec.js +++ b/test/unit/spec/narrative_core/upload/stagingAreaViewer-spec.js @@ -18,6 +18,7 @@ define ([ TestUtil ) { 'use strict'; + describe('Test the staging area viewer widget', function() { let stagingViewer, $targetNode = $('
'), @@ -27,7 +28,7 @@ define ([ beforeEach(function() { jasmine.Ajax.install(); - jasmine.Ajax.stubRequest(/.*\/staging_service\/list\/.*/).andReturn({ + jasmine.Ajax.stubRequest(/.*\/staging_service\/list\/?/).andReturn({ status: 200, statusText: 'success', contentType: 'text/plain', @@ -54,9 +55,14 @@ define ([ sidePanel: { '$dataWidget': { '$overlayPanel': {} + }, + '$methodsWidget': { + currentTag: 'release' } }, - showDataOverlay: () => {} + showDataOverlay: () => {}, + addAndPopulateApp: (id, tag, inputs) => {}, + hideOverlay: () => {}, }; stagingViewer = new StagingAreaViewer($targetNode, { path: startingPath, @@ -71,6 +77,7 @@ define ([ afterEach(() => { jasmine.Ajax.uninstall(); $targetNode.remove(); + stagingViewer = null; }); it('Should initialize properly', function() { @@ -82,7 +89,7 @@ define ([ expect(stagingViewer).not.toBeNull(); }); - it('Should render properly with a Globus linked account', () => { + it('Should render properly with a Globus linked account', (done) => { let $node = $('
'), linkedStagingViewer = new StagingAreaViewer($node, { path: startingPath, @@ -92,9 +99,12 @@ define ([ globusLinked: true } }); - linkedStagingViewer.render(); - expect($node.html()).toContain('Or upload to this staging area by using'); - expect($node.html()).toContain('https://app.globus.org/file-manager?destination_id=3aca022a-5e5b-11e6-8309-22000b97daec&destination_path=%2F' + fakeUser); + linkedStagingViewer.render() + .then(() => { + expect($node.html()).toContain('Or upload to this staging area by using'); + expect($node.html()).toContain('https://app.globus.org/file-manager?destination_id=3aca022a-5e5b-11e6-8309-22000b97daec&destination_path=%2F' + fakeUser); + done(); + }); }); it('Should render properly without a Globus linked account', () => { @@ -107,23 +117,93 @@ define ([ expect(stagingViewer.tour).not.toBeNull(); }); - xit('Should try to create a new import app with missing info', function() { - stagingViewer.initImportApp('foo', 'i_am_a_file'); - }); - - xit('Should try to create a new import app with appropriate info', function() { - stagingViewer.initImportApp('fba_model', 'i_am_a_file'); - }); - it('Should update its view with a proper subpath', function(done) { stagingViewer.updateView() .then(function() { done(); }) - .fail(err => { + .catch(err => { console.log(err); fail(); }); }); + + it('Should show an error when a path does not exist', (done, fail) => { + const errorText = 'An error occurred while fetching your files'; + jasmine.Ajax.stubRequest(/.*\/staging_service\/list\/foo?/).andReturn({ + status: 404, + statusText: 'success', + contentType: 'text/plain', + responseHeaders: '', + responseText: errorText + }); + + stagingViewer.setPath('//foo') + .then(() => { + expect($targetNode.find('.alert.alert-danger').html()).toContain(errorText); + // reset path. something gets cached with how async tests run. + stagingViewer.setPath('/'); + done(); + }); + }); + + it('Should show a "no files" next when a path has no files', (done) => { + jasmine.Ajax.stubRequest(/.*\/staging_service\/list\/empty?/).andReturn({ + status: 200, + statusText: 'success', + contentType: 'text/plain', + responseHeaders: '', + responseText: JSON.stringify([]) + }); + + stagingViewer.setPath('//empty') + .then(() => { + expect($targetNode.find('#kb-data-staging-table').html()).toContain('No files found.'); + // reset path. something gets cached with how async tests run. + stagingViewer.setPath('/'); + done(); + }); + }); + + it('Should respond to activate and deactivate commands', () => { + expect(stagingViewer.refreshInterval).toBeFalsy(); + stagingViewer.activate(); + expect(stagingViewer.refreshInterval).toBeDefined(); + stagingViewer.deactivate(); + expect(stagingViewer.refreshInterval).toBeUndefined(); + }); + + it('Should initialize an import app with the expected inputs', () => { + const fileType = 'fastq_reads', + fileName = 'foobar.txt', + appId = 'kb_uploadmethods/import_fastq_sra_as_reads_from_staging', + tag = Jupyter.narrative.sidePanel.$methodsWidget.currentTag, + inputs = { + fastq_fwd_staging_file_name: fileName, + name: fileName + '_reads', + import_type: 'FASTQ/FASTA' + }; + spyOn(Jupyter.narrative, 'addAndPopulateApp'); + spyOn(Jupyter.narrative, 'hideOverlay'); + stagingViewer.initImportApp(fileType, {name: fileName}); + expect(Jupyter.narrative.addAndPopulateApp).toHaveBeenCalledWith(appId, tag, inputs); + expect(Jupyter.narrative.hideOverlay).toHaveBeenCalled(); + }); + + it('Should NOT initialize an import app with an unknown type', () => { + spyOn(Jupyter.narrative, 'addAndPopulateApp'); + spyOn(Jupyter.narrative, 'hideOverlay'); + stagingViewer.initImportApp('some_unknown_type', 'foobar.txt'); + expect(Jupyter.narrative.addAndPopulateApp).not.toHaveBeenCalled(); + expect(Jupyter.narrative.hideOverlay).not.toHaveBeenCalled(); + }); + + it('Creates a downloader iframe when requested', () => { + stagingViewer.downloadFile('some_url'); + let dlNode = document.getElementById('hiddenDownloader'); + expect(dlNode).toBeDefined(); + expect(dlNode.getAttribute('src')).toEqual('some_url'); + }); + }); });