diff --git a/kbase-extension/static/kbase/css/kbaseNarrative.css b/kbase-extension/static/kbase/css/kbaseNarrative.css index 1b1e2137b7..e7e120dd70 100644 --- a/kbase-extension/static/kbase/css/kbaseNarrative.css +++ b/kbase-extension/static/kbase/css/kbaseNarrative.css @@ -88,6 +88,12 @@ cursor: pointer; } +.kb-data-staging__container { + height: 604px; + padding: 5px; + overflow-y: auto; +} + .kb-data-staging-footer { font-family: Oxygen, Arial, sans-serif; font-weight: bold; diff --git a/kbase-extension/static/kbase/js/widgets/narrative_core/kbaseNarrativeStagingDataTab.js b/kbase-extension/static/kbase/js/widgets/narrative_core/kbaseNarrativeStagingDataTab.js index a7ed4fb74d..73e8a300db 100644 --- a/kbase-extension/static/kbase/js/widgets/narrative_core/kbaseNarrativeStagingDataTab.js +++ b/kbase-extension/static/kbase/js/widgets/narrative_core/kbaseNarrativeStagingDataTab.js @@ -19,15 +19,18 @@ define([ return new KBWidget({ name: 'kbaseNarrativeStagingDataTab', $myFiles: $('
'), + minRefreshTime: 1000, // minimum ms required before letting updateView do its update + lastRefresh: 0, // the last time (in ms since epoch) that updateView was run + updateTimeout: null, // a Timeout that reconciles the above times (resets to null) init: function(options) { this._super(options); this.path = '/'; }, - getUserInfo: () => { - let auth = Auth.make({url: Config.url('auth')}); - var userInfo; + getUserInfo: function() { + const auth = Auth.make({url: Config.url('auth')}); + let userInfo; return auth.getCurrentProfile(auth.getAuthToken()) .then(info => { userInfo = { @@ -36,12 +39,12 @@ define([ }; return userInfo; }) - .catch((err) => { + .catch(() => { console.error('An error occurred while determining whether the user account is linked to Globus. Continuing without links.'); userInfo = { user: Jupyter.narrative.userId, globusLinked: false - } + }; return userInfo; }) .finally(() => { @@ -49,11 +52,11 @@ define([ }); }, - activate : function() { + activate: function() { this.stagingAreaViewer.activate(); }, - deactivate : function() { + deactivate: function() { this.stagingAreaViewer.deactivate(); }, @@ -65,38 +68,69 @@ define([ render: function() { return this.getUserInfo() - .then(userInfo => { - var $mainElem = $('
') - .css({ - 'height': '604px', - 'padding': '5px', - 'overflow-y': 'auto' + .then(userInfo => { + const $mainElem = $('
') + .addClass('kb-data-staging__container'); + const $dropzoneElem = $('
'); + this.$elem + .empty() + .append($mainElem + .append($dropzoneElem) + .append(this.$myFiles)); + + this.uploadWidget = new FileUploadWidget($dropzoneElem, { + path: this.path, + userInfo: userInfo, + userId: Jupyter.narrative.userId }); - var $dropzoneElem = $('
'); - this.$elem - .empty() - .append($mainElem - .append($dropzoneElem) - .append(this.$myFiles)); - this.uploadWidget = new FileUploadWidget($dropzoneElem, { - path: this.path, - userInfo: userInfo, - userId: Jupyter.narrative.userId - }); + this.uploadWidget.dropzone.on('complete', () => { + this.updateView(); + }); - this.stagingAreaViewer = new StagingAreaViewer(this.$myFiles, { - path: this.path, - updatePathFn: this.updatePath.bind(this), - userInfo: userInfo - }); + this.stagingAreaViewer = new StagingAreaViewer(this.$myFiles, { + path: this.path, + updatePathFn: this.updatePath.bind(this), + userInfo: userInfo + }); - this.updateView(); - }); + this.updateView(); + }); }, + /** + * This updates the staging area viewer, and is called whenever an upload finishes. + * In addition, this should only fire a refresh event once per second (or some interval) + * to avoid spamming the staging area service and locking up the browser. + * + * So, when this is called the first time, it tracks the time it was called. + * If the next time this is called is less than some minRefreshTime apart, this + * makes a timeout with the time difference. + */ updateView: function() { - this.stagingAreaViewer.render(); + // this does the staging area re-render, then tracks the time + // it was last done. + const renderStagingArea = () => { + this.stagingAreaViewer.render(); + this.lastRefresh = new Date().getTime(); + }; + + // See how long it's been since the last refresh. + const refreshDiff = new Date().getTime() - this.lastRefresh; + if (refreshDiff < this.minRefreshTime) { + // if it's been under the minimum refresh time, and we're not already sitting on + // a pending timeout, then make one. + // If there IS a pending timeout, then do nothing, and it'll refresh when that fires. + if (!this.updateTimeout) { + this.updateTimeout = setTimeout(() => { + renderStagingArea(); + this.updateTimeout = null; + }, refreshDiff); + } + } + else { + renderStagingArea(); + } } }); }); diff --git a/src/config.json b/src/config.json index f9268f2ab8..4edff1a286 100644 --- a/src/config.json +++ b/src/config.json @@ -103,7 +103,7 @@ "google_ad_conversion": "kR9OCLas4JgBEOy2pucC" }, "comm_wait_timeout": 600000, - "config": "narrative-refactor", + "config": "dev", "data_panel": { "initial_sort_limit": 10000, "max_name_length": 33, diff --git a/test/unit/spec/narrative_core/kbaseNarrativeStagingDataTab-spec.js b/test/unit/spec/narrative_core/kbaseNarrativeStagingDataTab-spec.js index db68fc5acd..e104aade05 100644 --- a/test/unit/spec/narrative_core/kbaseNarrativeStagingDataTab-spec.js +++ b/test/unit/spec/narrative_core/kbaseNarrativeStagingDataTab-spec.js @@ -1,7 +1,5 @@ /*global define*/ -/*global describe, it, expect*/ -/*global jasmine*/ -/*global beforeEach, afterEach*/ +/*global jasmine, describe, it, expect, beforeEach, afterEach, spyOn*/ /*jslint white: true*/ define([ 'jquery', @@ -14,12 +12,10 @@ define([ ) { 'use strict'; describe('Test the kbaseNarrativeStagingDataTab widget', () => { - let $dummyNode = $('
'), - stagingWidget, - fakeUser = 'notAUser'; + const fakeUser = 'notAUser'; beforeEach(() => { jasmine.Ajax.install(); - jasmine.Ajax.stubRequest("https://ci.kbase.us/services/auth/api/V2/me").andReturn({ + jasmine.Ajax.stubRequest(/\/auth\/api\/V2\/me$/).andReturn({ status: 200, statusText: 'success', contentType: 'application/json', @@ -42,14 +38,14 @@ define([ responseHeaders: '', responseText: JSON.stringify([ { - name: "test_folder", - path: fakeUser + "/test_folder", + name: 'test_folder', + path: fakeUser + '/test_folder', mtime: 1532738637499, size: 34, isFolder: true }, { - name: "file_list.txt", - path: fakeUser + "/test_folder/file_list.txt", + name: 'file_list.txt', + path: fakeUser + '/test_folder/file_list.txt', mtime: 1532738637555, size: 49233, source: 'KBase upload' @@ -60,30 +56,109 @@ define([ userId: fakeUser, getAuthToken: () => { return 'fakeToken'; } }; - stagingWidget = new StagingDataTab($dummyNode); }); afterEach(() => { jasmine.Ajax.uninstall(); - $dummyNode.remove(); }); - it('can load properly', (done) => { - stagingWidget.render() - .then(() => { - expect(stagingWidget).not.toBeNull(); - done(); - }); + it('can load properly', async () => { + const $dummyNode = $('
'), + stagingWidget = new StagingDataTab($dummyNode); + await stagingWidget.render(); + expect(stagingWidget).not.toBeNull(); }); - it('can update its path properly', (done) => { - stagingWidget.render() - .then(() => { - var newPath = 'a_new_path'; - stagingWidget.updatePath(newPath); - expect(stagingWidget.path).toEqual(newPath); - done(); - }); + it('properly catches failed user profile lookups by returning a default unlinked profile', async () => { + jasmine.Ajax.stubRequest(/\/auth\/api\/V2\/me$/).andReturn({ + status: 500, + statusText: 'error', + contentType: 'text/html', + responseHeaders: '', + responseText: 'error! no profile for you!' + }); + const $dummyNode = $('
'), + stagingWidget = new StagingDataTab($dummyNode); + const userInfo = await stagingWidget.getUserInfo(); + expect(userInfo).toEqual({user: fakeUser, globusLinked: false}); + }); + + it('gets user info and parsed into whether or not the user is linked to globus', async () => { + const $dummyNode = $('
'), + stagingWidget = new StagingDataTab($dummyNode); + const userInfo = await stagingWidget.getUserInfo(); + expect(userInfo).toEqual({user: fakeUser, globusLinked: true}); + }); + + it('can update its path properly', async () => { + const $dummyNode = $('
'), + stagingWidget = new StagingDataTab($dummyNode); + await stagingWidget.render(); + const newPath = 'a_new_path'; + stagingWidget.updatePath(newPath); + expect(stagingWidget.path).toEqual(newPath); + }); + + it('can activate its staging area viewer', async () => { + const $dummyNode = $('
'), + stagingWidget = new StagingDataTab($dummyNode); + await stagingWidget.render(); + spyOn(stagingWidget.stagingAreaViewer, 'activate'); + stagingWidget.activate(); + expect(stagingWidget.stagingAreaViewer.activate).toHaveBeenCalled(); + }); + + it('can deactivate its staging area viewer', async () => { + const $dummyNode = $('
'), + stagingWidget = new StagingDataTab($dummyNode); + await stagingWidget.render(); + spyOn(stagingWidget.stagingAreaViewer, 'deactivate'); + stagingWidget.activate(); + stagingWidget.deactivate(); + expect(stagingWidget.stagingAreaViewer.deactivate).toHaveBeenCalled(); + }); + + it('can be told to update its view', async () => { + jasmine.clock().install(); + + const $dummyNode = $('
'), + stagingWidget = new StagingDataTab($dummyNode); + await stagingWidget.render(); + // kinda cheating - but to test that things are run, we know that this will call + // stagingAreaViewer.render + spyOn(stagingWidget.stagingAreaViewer, 'render'); + stagingWidget.updateView(); + jasmine.clock().tick(stagingWidget.minRefreshTime + 100); + expect(stagingWidget.stagingAreaViewer.render).toHaveBeenCalled(); + + jasmine.clock().uninstall(); + }); + + it('can be triggered to update its view after a completed upload', async () => { + const $dummyNode = $('
'), + stagingWidget = new StagingDataTab($dummyNode); + await stagingWidget.render(); + spyOn(stagingWidget, 'updateView'); + // a little more cheating with implementation, this triggers the "upload complete" event + stagingWidget.uploadWidget.dropzone.emit('complete', {name: 'foo', size: 12345}); + expect(stagingWidget.updateView).toHaveBeenCalled(); + }); + + it('only updates its view once per interval on completed uploads', async () => { + jasmine.clock().install(); + + const $dummyNode = $('
'), + stagingWidget = new StagingDataTab($dummyNode); + await stagingWidget.render(); + // run a bunch of triggers, should only call render on the staging area once + spyOn(stagingWidget.stagingAreaViewer, 'render'); + for (let i = 0; i < 100; i++) { + stagingWidget.uploadWidget.dropzone.emit('complete', {name: 'foo', size: 12345}); + } + jasmine.clock().tick(stagingWidget.minRefreshTime + 100); + expect(stagingWidget.stagingAreaViewer.render.calls.count()).toEqual(1); + + jasmine.clock().uninstall(); }); }); });