Skip to content

Commit

Permalink
Merge pull request #1886 from kbase/DATAUP-266-staging-refresh
Browse files Browse the repository at this point in the history
DATAUP-266 staging refresh
  • Loading branch information
briehl committed Oct 23, 2020
2 parents 742c3be + f0490ff commit a30f5aa
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 60 deletions.
6 changes: 6 additions & 0 deletions kbase-extension/static/kbase/css/kbaseNarrative.css
Expand Up @@ -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;
Expand Down
Expand Up @@ -19,15 +19,18 @@ define([
return new KBWidget({
name: 'kbaseNarrativeStagingDataTab',
$myFiles: $('<div>'),
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 = {
Expand All @@ -36,24 +39,24 @@ 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(() => {
return userInfo;
});
},

activate : function() {
activate: function() {
this.stagingAreaViewer.activate();
},

deactivate : function() {
deactivate: function() {
this.stagingAreaViewer.deactivate();
},

Expand All @@ -65,38 +68,69 @@ define([

render: function() {
return this.getUserInfo()
.then(userInfo => {
var $mainElem = $('<div>')
.css({
'height': '604px',
'padding': '5px',
'overflow-y': 'auto'
.then(userInfo => {
const $mainElem = $('<div>')
.addClass('kb-data-staging__container');
const $dropzoneElem = $('<div>');
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 = $('<div>');
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();
}
}
});
});
2 changes: 1 addition & 1 deletion src/config.json
Expand Up @@ -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,
Expand Down
129 changes: 102 additions & 27 deletions 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',
Expand All @@ -14,12 +12,10 @@ define([
) {
'use strict';
describe('Test the kbaseNarrativeStagingDataTab widget', () => {
let $dummyNode = $('<div>'),
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',
Expand All @@ -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'
Expand All @@ -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 = $('<div>'),
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 = $('<div>'),
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 = $('<div>'),
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 = $('<div>'),
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 = $('<div>'),
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 = $('<div>'),
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 = $('<div>'),
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 = $('<div>'),
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 = $('<div>'),
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();
});
});
});

0 comments on commit a30f5aa

Please sign in to comment.