From 0f0a82389910b94f418f45a42101b083e660431c Mon Sep 17 00:00:00 2001 From: Bill Riehl Date: Tue, 29 Sep 2020 14:23:59 -0700 Subject: [PATCH 01/13] add different event to startup --- kbase-extension/static/kbase/js/kbaseNarrative.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/kbase-extension/static/kbase/js/kbaseNarrative.js b/kbase-extension/static/kbase/js/kbaseNarrative.js index db7addf7c0..74d4eef3bd 100644 --- a/kbase-extension/static/kbase/js/kbaseNarrative.js +++ b/kbase-extension/static/kbase/js/kbaseNarrative.js @@ -292,7 +292,6 @@ define([ isReady: Jupyter.notebook.kernel && Jupyter.notebook.kernel.is_connected() } ); - console.log('emitted kernel-state-changed event, probably not ready!'); }); }); $([Jupyter.events]).on('delete.Cell', function () { @@ -784,7 +783,7 @@ define([ this.sidePanel.render(); }.bind(this)); - $([Jupyter.events]).on('kernel_ready.Kernel', + $([Jupyter.events]).on('kernel_connected.Kernel', (e) => { this.loadingWidget.updateProgress('kernel', true); this.jobCommChannel = new JobCommChannel(); From 0b0b9a5625da40175bd94dce42a88ef40cf6e103 Mon Sep 17 00:00:00 2001 From: Bill Riehl Date: Sun, 4 Oct 2020 23:42:17 -0700 Subject: [PATCH 02/13] add a different event for detecting kernel connections, some cleanup and additional tests --- .../static/kbase/js/kbaseNarrative.js | 2006 +++++++++-------- test/unit/spec/kbaseNarrativeSpec.js | 102 +- 2 files changed, 1185 insertions(+), 923 deletions(-) diff --git a/kbase-extension/static/kbase/js/kbaseNarrative.js b/kbase-extension/static/kbase/js/kbaseNarrative.js index 74d4eef3bd..64d84021f8 100644 --- a/kbase-extension/static/kbase/js/kbaseNarrative.js +++ b/kbase-extension/static/kbase/js/kbaseNarrative.js @@ -42,9 +42,7 @@ define([ 'kb_service/utils', 'widgets/loadingWidget', 'kb_service/client/workspace', - // for effect 'bootstrap', - ], function ( $, Promise, @@ -85,19 +83,19 @@ define([ KBaseNarrativePrestart.loadJupyterEvents(); /** - * @constructor - * The base, namespaced Narrative object. This is mainly used at start-up time, and - * gets injected into the Jupyter namespace. - * - * Most of its methods below - init, registerEvents, initAboutDialog, initUpgradeDialog, - * checkVersion, updateVersion - are set up at startup time. - * This is all done by an injection into static/notebook/js/main.js where the - * Narrative object is set up, and Narrative.init is run. - * - * But, this also has a noteable 'Save' method, that implements another Narrative- - * specific piece of functionality. See Narrative.prototype.saveNarrative below. - */ - var Narrative = function () { + * @constructor + * The base, namespaced Narrative object. This is mainly used at start-up time, and + * gets injected into the Jupyter namespace. + * + * Most of its methods below - init, registerEvents, initAboutDialog, initUpgradeDialog, + * checkVersion, updateVersion - are set up at startup time. + * This is all done by an injection into static/notebook/js/main.js where the + * Narrative object is set up, and Narrative.init is run. + * + * But, this also has a noteable 'Save' method, that implements another Narrative- + * specific piece of functionality. See Narrative.prototype.saveNarrative below. + */ + const Narrative = function () { // Maximum narrative size that can be stored in the workspace. // This is set by nginx on the backend - this variable is just for // communication on error. @@ -149,14 +147,14 @@ define([ this.loadingWidget = new LoadingWidget({ node: document.querySelector('#kb-loading-blocker'), - timeout: 20000 + timeout: 20000, }); //Jupyter.keyboard_manager.disable(); return this; }; - Narrative.prototype.isLoaded = function () { + Narrative.prototype.isLoaded = () => { return Jupyter.notebook._fully_loaded; }; @@ -165,931 +163,1115 @@ define([ return testMode.toLowerCase() === uiMode; }; - Narrative.prototype.getAuthToken = function () { - return NarrativeLogin.getAuthToken(); - }; - - Narrative.prototype.getNarrativeRef = function () { - return Promise.try(() => { - if (this.workspaceRef) { - return this.workspaceRef; - } - else { - return new Workspace(Config.url('workspace'), {token: this.getAuthToken()}) - .get_workspace_info({id: this.workspaceId}) - .then((wsInfo) => { - let narrId = wsInfo[8]['narrative']; - this.workspaceRef = this.workspaceId + '/' + narrId; - return this.workspaceRef; - }); - } - }); - }; - - Narrative.prototype.getUserPermissions = function () { - return new Workspace(Config.url('workspace'), {token: this.getAuthToken()}) - .get_workspace_info({id: this.workspaceId}) - .then((wsInfo) => { - return wsInfo[5]; - }); - } - - /** - * A wrapper around the Jupyter.notebook.kernel.execute() function. - * If any KBase widget needs to make a kernel call, it should go through here. - * ...when it's done. - */ - Narrative.prototype.executeKernelCall = function () { - console.info('no-op for now'); - }; - - // Wrappers for the Jupyter/Jupyter function so we only maintain it in one place. - Narrative.prototype.patchKeyboardMapping = function () { - var commonShortcuts = [ - 'a', 'm', 'f', 'y', 'r', - '1', '2', '3', '4', '5', '6', - 'k', 'j', 'b', 'x', 'c', 'v', - 'z', 'd,d', 's', 'l', 'o', 'h', - 'i,i', '0,0', 'q', 'shift-j', 'shift-k', - 'shift-m', 'shift-o', 'shift-v' - ], - commandShortcuts = [], - editShortcuts = [ - // remove the command palette - // since it exposes commands we have "disabled" - // by removing keyboard mappings - 'cmdtrl-shift-p', - ]; - - commonShortcuts.forEach(function (shortcut) { - try { - Jupyter.keyboard_manager.command_shortcuts.remove_shortcut(shortcut); - } catch (ex) { - console.warn('Error removing shortcut "' + shortcut + '"', ex); - } - try { - Jupyter.notebook.keyboard_manager.edit_shortcuts.remove_shortcut(shortcut); - } catch (ex) { - // console.warn('Error removing shortcut "' + shortcut +'"', ex); - } - }); - - commandShortcuts.forEach(function (shortcut) { - try { - Jupyter.keyboard_manager.command_shortcuts.remove_shortcut(shortcut); - } catch (ex) { - console.warn('Error removing shortcut "' + shortcut + '"', ex); - } - }); - - editShortcuts.forEach(function (shortcut) { - try { - Jupyter.notebook.keyboard_manager.edit_shortcuts.remove_shortcut(shortcut); - } catch (ex) { - console.warn('Error removing shortcut "' + shortcut + '"', ex); - } - }); - }; - - Narrative.prototype.disableKeyboardManager = function () { - Jupyter.keyboard_manager.disable(); - }; - - Narrative.prototype.enableKeyboardManager = function () { - // Jupyter.keyboard_manager.enable(); - }; - - /** - * Registers Narrative responses to a few Jupyter events - mainly some - * visual effects for managing when the cell toolbar should be shown, - * and when saving is being done, but it also disables the keyboard - * manager when KBase cells are selected. - */ - Narrative.prototype.registerEvents = function () { - var self = this; - $([Jupyter.events]).on('before_save.Notebook', function () { - $('#kb-save-btn').find('div.fa-save').addClass('fa-spin'); - }); - $([Jupyter.events]).on('notebook_saved.Notebook', function () { - $('#kb-save-btn').find('div.fa-save').removeClass('fa-spin'); - self.stopVersionCheck = false; - self.updateDocumentVersion(); - }); - $([Jupyter.events]).on('kernel_idle.Kernel', function () { - $('#kb-kernel-icon').removeClass().addClass('fa fa-circle-o'); - }); - $([Jupyter.events]).on('kernel_busy.Kernel', function () { - $('#kb-kernel-icon').removeClass().addClass('fa fa-circle'); + Narrative.prototype.getAuthToken = function () { + return NarrativeLogin.getAuthToken(); + }; + + Narrative.prototype.getNarrativeRef = function () { + return Promise.try(() => { + if (this.workspaceRef) { + return this.workspaceRef; + } else { + return new Workspace(Config.url('workspace'), { + token: this.getAuthToken(), + }) + .get_workspace_info({ id: this.workspaceId }) + .then((wsInfo) => { + let narrId = wsInfo[8]['narrative']; + this.workspaceRef = this.workspaceId + '/' + narrId; + return this.workspaceRef; + }); + } + }); + }; + + Narrative.prototype.getUserPermissions = function () { + return new Workspace(Config.url('workspace'), { + token: this.getAuthToken(), + }) + .get_workspace_info({ id: this.workspaceId }) + .then((wsInfo) => { + return wsInfo[5]; + }); + }; + + /** + * A wrapper around the Jupyter.notebook.kernel.execute() function. + * If any KBase widget needs to make a kernel call, it should go through here. + * ...when it's done. + */ + Narrative.prototype.executeKernelCall = function () { + console.info('no-op for now'); + }; + + // Wrappers for the Jupyter/Jupyter function so we only maintain it in one place. + Narrative.prototype.patchKeyboardMapping = function () { + var commonShortcuts = [ + 'a', + 'm', + 'f', + 'y', + 'r', + '1', + '2', + '3', + '4', + '5', + '6', + 'k', + 'j', + 'b', + 'x', + 'c', + 'v', + 'z', + 'd,d', + 's', + 'l', + 'o', + 'h', + 'i,i', + '0,0', + 'q', + 'shift-j', + 'shift-k', + 'shift-m', + 'shift-o', + 'shift-v', + ], + commandShortcuts = [], + editShortcuts = [ + // remove the command palette + // since it exposes commands we have 'disabled' + // by removing keyboard mappings + 'cmdtrl-shift-p', + ]; + + commonShortcuts.forEach(function (shortcut) { + try { + Jupyter.keyboard_manager.command_shortcuts.remove_shortcut(shortcut); + } catch (ex) { + console.warn('Error removing shortcut "' + shortcut + '"', ex); + } + try { + Jupyter.notebook.keyboard_manager.edit_shortcuts.remove_shortcut( + shortcut + ); + } catch (ex) { + // console.warn('Error removing shortcut '' + shortcut +'"', ex); + } + }); + + commandShortcuts.forEach(function (shortcut) { + try { + Jupyter.keyboard_manager.command_shortcuts.remove_shortcut(shortcut); + } catch (ex) { + console.warn('Error removing shortcut "' + shortcut + '"', ex); + } + }); + + editShortcuts.forEach(function (shortcut) { + try { + Jupyter.notebook.keyboard_manager.edit_shortcuts.remove_shortcut( + shortcut + ); + } catch (ex) { + console.warn('Error removing shortcut "' + shortcut + '"', ex); + } + }); + }; + + Narrative.prototype.disableKeyboardManager = function () { + Jupyter.keyboard_manager.disable(); + }; + + Narrative.prototype.enableKeyboardManager = function () { + // Jupyter.keyboard_manager.enable(); + }; + + /** + * Registers Narrative responses to a few Jupyter events - mainly some + * visual effects for managing when the cell toolbar should be shown, + * and when saving is being done, but it also disables the keyboard + * manager when KBase cells are selected. + */ + Narrative.prototype.registerEvents = function () { + var self = this; + $([Jupyter.events]).on('before_save.Notebook', function () { + $('#kb-save-btn').find('div.fa-save').addClass('fa-spin'); + }); + $([Jupyter.events]).on('notebook_saved.Notebook', function () { + $('#kb-save-btn').find('div.fa-save').removeClass('fa-spin'); + self.stopVersionCheck = false; + self.updateDocumentVersion(); + }); + $([Jupyter.events]).on('kernel_idle.Kernel', function () { + $('#kb-kernel-icon').removeClass().addClass('fa fa-circle-o'); + }); + $([Jupyter.events]).on('kernel_busy.Kernel', function () { + $('#kb-kernel-icon').removeClass().addClass('fa fa-circle'); + }); + [ + 'kernel_connected.Kernel', + 'kernel_starting.Kernel', + 'kernel_ready.Kernel', + 'kernel_disconnected.Kernel', + 'kernel_killed.Kernel', + 'kernel_dead.Kernel', + ].forEach(function (e) { + $([Jupyter.events]).on(e, function () { + self.runtime.bus().emit('kernel-state-changed', { + isReady: + Jupyter.notebook.kernel && Jupyter.notebook.kernel.is_connected(), }); - [ - 'kernel_connected.Kernel', 'kernel_starting.Kernel', 'kernel_ready.Kernel', - 'kernel_disconnected.Kernel', 'kernel_killed.Kernel', 'kernel_dead.Kernel' - ].forEach(function(e) { - $([Jupyter.events]).on(e, function () { - self.runtime.bus().emit( - 'kernel-state-changed', - { - isReady: Jupyter.notebook.kernel && Jupyter.notebook.kernel.is_connected() - } - ); - }); + }); + }); + $([Jupyter.events]).on( + 'delete.Cell', + function () { + // this.enableKeyboardManager(); + }.bind(this) + ); + + $([Jupyter.events]).on( + 'notebook_save_failed.Notebook', + function (event, data) { + $('#kb-save-btn').find('div.fa-save').removeClass('fa-spin'); + this.saveFailed(event, data); + }.bind(this) + ); + }; + + /** + * Initializes the sharing panel and sets up the events + * that show and hide it. + * + * This is a hack and a half because Select2, Bootstrap, + * and Safari are all hateful things. Here are the sequence of + * events. + * 1. Initialize the dialog object. + * 2. When it gets invoked, show the dialog. + * 3. On the FIRST time it gets shown, after it's done + * being rendered (shown.bs.modal event), then build and + * show the share panel widget. The select2 thing only wants + * to appear and behave correctly after the page loads, and + * after there's a visible DOM element for it to render in. + */ + Narrative.prototype.initSharePanel = function () { + var sharePanel = $( + '


' + ), + shareWidget = null, + shareDialog = new BootstrapDialog({ + title: 'Change Share Settings', + body: sharePanel, + closeButton: true, + }); + shareDialog.getElement().one( + "shown.bs.modal", + function () { + shareWidget = new KBaseNarrativeSharePanel(sharePanel.empty(), { + ws_name_or_id: this.getWorkspaceName(), }); - $([Jupyter.events]).on('delete.Cell', function () { - // this.enableKeyboardManager(); - }.bind(this)); - - $([Jupyter.events]).on('notebook_save_failed.Notebook', function (event, data) { - $('#kb-save-btn').find('div.fa-save').removeClass('fa-spin'); - this.saveFailed(event, data); - }.bind(this)); - }; - - - /** - * Initializes the sharing panel and sets up the events - * that show and hide it. - * - * This is a hack and a half because Select2, Bootstrap, - * and Safari are all hateful things. Here are the sequence of - * events. - * 1. Initialize the dialog object. - * 2. When it gets invoked, show the dialog. - * 3. On the FIRST time it gets shown, after it's done - * being rendered (shown.bs.modal event), then build and - * show the share panel widget. The select2 thing only wants - * to appear and behave correctly after the page loads, and - * after there's a visible DOM element for it to render in. - */ - Narrative.prototype.initSharePanel = function () { - var sharePanel = $('


'), - shareWidget = null, - shareDialog = new BootstrapDialog({ - title: 'Change Share Settings', - body: sharePanel, - closeButton: true - }); - shareDialog.getElement().one('shown.bs.modal', function () { - shareWidget = new KBaseNarrativeSharePanel(sharePanel.empty(), { - ws_name_or_id: this.getWorkspaceName() - }); - }.bind(this)); - $('#kb-share-btn').click(function () { - var narrName = Jupyter.notebook.notebook_name; - if (narrName.trim().toLowerCase() === 'untitled' || narrName.trim().length === 0) { - Jupyter.save_widget.rename_notebook({ - notebook: Jupyter.notebook, - message: 'Please name your Narrative before sharing.', - callback: function () { shareDialog.show(); } - }); - return; - } - if (shareWidget) { - shareWidget.refresh(); - } - shareDialog.show(); - }.bind(this)); - }; - - Narrative.prototype.initStaticNarrativesPanel = function () { - if (!Config.get('features').staticNarratives) { - $('#kb-static-btn').remove(); - return; + }.bind(this) + ); + $("#kb-share-btn").click( + function () { + var narrName = Jupyter.notebook.notebook_name; + if ( + narrName.trim().toLowerCase() === "untitled" || + narrName.trim().length === 0 + ) { + Jupyter.save_widget.rename_notebook({ + notebook: Jupyter.notebook, + message: "Please name your Narrative before sharing.", + callback: function () { + shareDialog.show(); + }, + }); + return; } - const staticPanel = $('
'), - staticDialog = new BootstrapDialog({ - title: 'Static Narratives', - body: staticPanel, - closeButton: true - }), - staticWidget = new StaticNarrativesPanel(staticPanel); - $('#kb-static-btn').click(() => { - staticWidget.refresh(); - staticDialog.show(); - }); - }; - - /** - * Expects docInfo to be a workspace object info array, especially where the 4th element is - * an int > 0. - */ - Narrative.prototype.checkDocumentVersion = function (docInfo) { - if (docInfo.length < 5 || this.stopVersionCheck) { - return; + if (shareWidget) { + shareWidget.refresh(); } - if (docInfo[4] !== this.documentVersionInfo[4]) { - // now we make the dialog and all that. - $('#kb-narr-version-btn') - .off('click') - .on('click', function() { - this.showDocumentVersionDialog(docInfo); - }.bind(this)); - this.toggleDocumentVersionBtn(true); + shareDialog.show(); + }.bind(this) + ); + }; + + Narrative.prototype.initStaticNarrativesPanel = function () { + if (!Config.get("features").staticNarratives) { + $("#kb-static-btn").remove(); + return; + } + const staticPanel = $("
"), + staticDialog = new BootstrapDialog({ + title: "Static Narratives", + body: staticPanel, + closeButton: true, + }), + staticWidget = new StaticNarrativesPanel(staticPanel); + $("#kb-static-btn").click(() => { + staticWidget.refresh(); + staticDialog.show(); + }); + }; + + /** + * Expects docInfo to be a workspace object info array, especially where the 4th element is + * an int > 0. + */ + Narrative.prototype.checkDocumentVersion = function (docInfo) { + if (docInfo.length < 5 || this.stopVersionCheck) { + return; + } + if (docInfo[4] !== this.documentVersionInfo[4]) { + // now we make the dialog and all that. + $("#kb-narr-version-btn") + .off("click") + .on( + "click", + function () { + this.showDocumentVersionDialog(docInfo); + }.bind(this) + ); + this.toggleDocumentVersionBtn(true); + } + }; + + /** + * Expects the usual workspace object info array. If that's present, it's captured. If not, + * we run get_object_info_new and fetch it ourselves. Note that it should have its metadata. + */ + Narrative.prototype.updateDocumentVersion = function (docInfo) { + var self = this; + return Promise.try(function () { + if (docInfo) { + self.documentVersionInfo = docInfo; + } else { + var workspace = new Workspace(Config.url("workspace"), { + token: self.getAuthToken(), + }); + self + .getNarrativeRef() + .then((narrativeRef) => { + return workspace.get_object_info_new({ + objects: [{ ref: narrativeRef }], + includeMetadata: 1, + }); + }) + .then(function (info) { + self.documentVersionInfo = info[0]; + }) + .catch(function (error) { + // no op for now. + console.error(error); + }); + } + }); + }; + + Narrative.prototype.showDocumentVersionDialog = function (newVerInfo) { + var bodyTemplate = Handlebars.compile(DocumentVersionDialogBodyTemplate); + + var versionDialog = new BootstrapDialog({ + title: "Showing an older Narrative document", + body: bodyTemplate({ + currentVer: this.documentVersionInfo, + currentDate: TimeFormat.readableTimestamp(this.documentVersionInfo[3]), + newVer: newVerInfo, + newDate: TimeFormat.readableTimestamp(newVerInfo[3]), + sameUser: this.documentVersionInfo[5] === newVerInfo[5], + readOnly: this.readonly, + }), + alertOnly: true, + }); + + versionDialog.show(); + }; + + /** + * @method + * @public + * This shows or hides the "narrative has been saved in a different window" button. + * If show is truthy, show it. Otherwise, hide it. + */ + Narrative.prototype.toggleDocumentVersionBtn = function (show) { + var $btn = $("#kb-narr-version-btn"); + if (show && !$btn.is(":visible")) { + $btn.fadeIn("fast"); + } else if (!show && $btn.is(":visible")) { + $btn.fadeOut("fast"); + } + }; + + /** + * The "Upgrade your container" dialog should be made available when + * there's a more recent version of the Narrative ready to use. This + * dialog then lets the user shut down their existing Narrative container. + */ + Narrative.prototype.initUpgradeDialog = function () { + var bodyTemplate = Handlebars.compile(UpdateDialogBodyTemplate); + + var $cancelBtn = $('