Sfoglia il codice sorgente

HUE-9000 [editor] Add separate serializer for notebooks to support changed execution structure

Johan Ahlen 6 anni fa
parent
commit
c1b6a14297

+ 6 - 2
desktop/core/src/desktop/js/api/apiHelper.js

@@ -1976,12 +1976,16 @@ class ApiHelper {
     return this.simplePost('/notebook/api/optimizer/statement/similarity', data);
   }
 
-  saveNotebook(options) {
+  async saveNotebook(options) {
     const data = {
       notebook: options.notebookJson,
       editorMode: options.editorMode
     };
-    return this.simplePost('/notebook/api/notebook/save', data);
+    return new Promise((resolve, reject) => {
+      this.simplePost('/notebook/api/notebook/save', data)
+        .then(resolve)
+        .catch(reject);
+    });
   }
 
   async getHistory(options) {

+ 12 - 8
desktop/core/src/desktop/js/apps/notebook2/components/ko.snippetExecuteButtonActions.js

@@ -14,27 +14,31 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import $ from 'jquery';
 import ko from 'knockout';
 
 import componentUtils from 'ko/components/componentUtils';
-import dataCatalog from 'catalog/dataCatalog';
 import huePubSub from 'utils/huePubSub';
 import I18n from 'utils/i18n';
-import { EXECUTION_STATUS } from "../execution/executableStatement";
+import { EXECUTION_STATUS } from '../execution/executableStatement';
 
 const TEMPLATE = `
 <div class="snippet-execute-actions">
   <div class="btn-group">
-    <!-- ko if: status() !== '${ EXECUTION_STATUS.running }' -->
-    <button class="btn btn-primary btn-mini btn-execute" data-bind="click: execute"><i class="fa fa-play fa-fw"></i> ${ I18n('Execute') }</button>
+    <!-- ko if: status() !== '${EXECUTION_STATUS.running}' -->
+    <button class="btn btn-primary btn-mini btn-execute" data-bind="click: execute"><i class="fa fa-play fa-fw"></i> ${I18n(
+      'Execute'
+    )}</button>
     <!-- /ko -->
-    <!-- ko if: status() === '${ EXECUTION_STATUS.running }' -->
+    <!-- ko if: status() === '${EXECUTION_STATUS.running}' -->
     <!-- ko ifnot: stopping -->
-    <button class="btn btn-primary btn-mini btn-execute" data-bind="click: stop"><i class="fa fa-stop fa-fw"></i> ${ I18n('Stop') }</button>
+    <button class="btn btn-primary btn-mini btn-execute" data-bind="click: stop"><i class="fa fa-stop fa-fw"></i> ${I18n(
+      'Stop'
+    )}</button>
     <!-- /ko -->
     <!-- ko if: stopping -->
-    <button class="btn btn-primary btn-mini btn-execute disabled"><i class="fa fa-spinner fa-spin fa-fw"></i> ${ I18n('Stop') }</button>
+    <button class="btn btn-primary btn-mini btn-execute disabled"><i class="fa fa-spinner fa-spin fa-fw"></i> ${I18n(
+      'Stop'
+    )}</button>
     <!-- /ko -->
     <!-- /ko -->
   </div>

+ 64 - 71
desktop/core/src/desktop/js/apps/notebook2/editorViewModel.js

@@ -23,8 +23,8 @@ import ChartTransformers from 'apps/notebook/chartTransformers';
 import huePubSub from 'utils/huePubSub';
 import hueUtils from 'utils/hueUtils';
 
-import { Notebook } from 'apps/notebook2/notebook';
-import { Snippet } from 'apps/notebook2/snippet';
+import Notebook from 'apps/notebook2/notebook';
+import Snippet from 'apps/notebook2/snippet';
 
 class EditorViewModel {
   constructor(editorId, notebooks, options, CoordinatorEditorViewModel, RunningCoordinatorModel) {
@@ -328,13 +328,12 @@ class EditorViewModel {
   }
 
   loadNotebook(notebookRaw, queryTab) {
-    const self = this;
     let currentQueries;
-    if (self.selectedNotebook() != null) {
-      currentQueries = self.selectedNotebook().unload();
+    if (this.selectedNotebook() != null) {
+      currentQueries = this.selectedNotebook().unload();
     }
 
-    const notebook = new Notebook(self, notebookRaw);
+    const notebook = new Notebook(this, notebookRaw);
 
     if (notebook.snippets().length > 0) {
       huePubSub.publish('detach.scrolls', notebook.snippets()[0]);
@@ -364,7 +363,7 @@ class EditorViewModel {
           snippet.result.statement_range.valueHasMutated();
         }
 
-        snippet.previousChartOptions = self.getPreviousChartOptions(snippet);
+        snippet.previousChartOptions = this.getPreviousChartOptions(snippet);
       });
 
       if (notebook.snippets()[0].result.data().length > 0) {
@@ -381,94 +380,89 @@ class EditorViewModel {
       }
     }
 
-    self.selectedNotebook(notebook);
+    this.selectedNotebook(notebook);
     huePubSub.publish('check.job.browser');
     huePubSub.publish('recalculate.name.description.width');
   }
 
-  newNotebook(editorType, callback, queryTab) {
-    const self = this;
-    huePubSub.publish('active.snippet.type.changed', {
-      type: editorType,
-      isSqlDialect: editorType ? self.getSnippetViewSettings(editorType).sqlDialect : undefined
-    });
-    $.post(
-      '/notebook/api/create_notebook',
-      {
-        type: editorType || self.editorType(),
+  async newNotebook(editorType, callback, queryTab) {
+    return new Promise((resolve, reject) => {
+      huePubSub.publish('active.snippet.type.changed', {
+        type: editorType,
+        isSqlDialect: editorType ? this.getSnippetViewSettings(editorType).sqlDialect : undefined
+      });
+      $.post('/notebook/api/create_notebook', {
+        type: editorType || this.editorType(),
         directory_uuid: window.location.getParameter('directory_uuid')
-      },
-      data => {
-        self.loadNotebook(data.notebook);
-        if (self.editorMode() && !self.isNotificationManager()) {
-          const snippet = self.selectedNotebook().newSnippet(self.editorType());
-          if (
-            queryTab &&
-            ['queryHistory', 'savedQueries', 'queryBuilderTab'].indexOf(queryTab) > -1
-          ) {
-            snippet.currentQueryTab(queryTab);
-          }
-          huePubSub.publish('detach.scrolls', self.selectedNotebook().snippets()[0]);
-          if (window.location.getParameter('type') === '') {
-            hueUtils.changeURLParameter('type', self.editorType());
+      })
+        .then(data => {
+          this.loadNotebook(data.notebook);
+          if (this.editorMode() && !this.isNotificationManager()) {
+            const snippet = this.selectedNotebook().newSnippet(this.editorType());
+            if (
+              queryTab &&
+              ['queryHistory', 'savedQueries', 'queryBuilderTab'].indexOf(queryTab) > -1
+            ) {
+              snippet.currentQueryTab(queryTab);
+            }
+            huePubSub.publish('detach.scrolls', this.selectedNotebook().snippets()[0]);
+            if (window.location.getParameter('type') === '') {
+              hueUtils.changeURLParameter('type', this.editorType());
+            }
+            huePubSub.publish('active.snippet.type.changed', {
+              type: editorType,
+              isSqlDialect: editorType
+                ? this.getSnippetViewSettings(editorType).sqlDialect
+                : undefined
+            });
           }
-          huePubSub.publish('active.snippet.type.changed', {
-            type: editorType,
-            isSqlDialect: editorType
-              ? self.getSnippetViewSettings(editorType).sqlDialect
-              : undefined
-          });
-        }
 
-        if (typeof callback !== 'undefined' && callback !== null) {
-          callback();
-        }
-      }
-    );
+          if (typeof callback !== 'undefined' && callback !== null) {
+            callback();
+          }
+          resolve();
+        })
+        .catch(reject);
+    });
   }
 
-  openNotebook(uuid, queryTab, skipUrlChange, callback) {
-    const self = this;
-    const deferredOpen = new $.Deferred();
-    $.get(
-      '/desktop/api2/doc/',
-      {
+  async openNotebook(uuid, queryTab, skipUrlChange, callback) {
+    return new Promise((resolve, reject) => {
+      $.get('/desktop/api2/doc/', {
         uuid: uuid,
         data: true,
         dependencies: true
-      },
-      data => {
+      }).then(data => {
         if (data.status === 0) {
           data.data.dependents = data.dependents;
           data.data.can_write = data.user_perms.can_write;
-          const notebook = data.data;
-          self.loadNotebook(notebook, queryTab);
-          if (typeof skipUrlChange === 'undefined' && !self.isNotificationManager()) {
-            if (self.editorMode()) {
-              self.editorType(data.document.type.substring('query-'.length));
+          const notebookRaw = data.data;
+          this.loadNotebook(notebookRaw, queryTab);
+          if (typeof skipUrlChange === 'undefined' && !this.isNotificationManager()) {
+            if (this.editorMode()) {
+              this.editorType(data.document.type.substring('query-'.length));
               huePubSub.publish('active.snippet.type.changed', {
-                type: self.editorType(),
-                isSqlDialect: self.getSnippetViewSettings(self.editorType()).sqlDialect
+                type: this.editorType(),
+                isSqlDialect: this.getSnippetViewSettings(this.editorType()).sqlDialect
               });
-              self.changeURL(
-                self.URLS.editor + '?editor=' + data.document.id + '&type=' + self.editorType()
+              this.changeURL(
+                this.URLS.editor + '?editor=' + data.document.id + '&type=' + this.editorType()
               );
             } else {
-              self.changeURL(self.URLS.notebook + '?notebook=' + data.document.id);
+              this.changeURL(this.URLS.notebook + '?notebook=' + data.document.id);
             }
           }
           if (typeof callback !== 'undefined') {
             callback();
           }
-          deferredOpen.resolve();
+          resolve();
         } else {
           $(document).trigger('error', data.message);
-          deferredOpen.reject();
-          self.newNotebook();
+          reject();
+          this.newNotebook();
         }
-      }
-    );
-    return deferredOpen.promise();
+      });
+    });
   }
 
   prepareShareModal() {
@@ -508,9 +502,8 @@ class EditorViewModel {
     });
   }
 
-  saveNotebook() {
-    const self = this;
-    self.selectedNotebook().save();
+  async saveNotebook() {
+    await this.selectedNotebook().save();
   }
 
   showContextPopover(field, event) {

+ 215 - 302
desktop/core/src/desktop/js/apps/notebook2/notebook.js

@@ -23,67 +23,32 @@ import hueAnalytics from 'utils/hueAnalytics';
 import huePubSub from 'utils/huePubSub';
 import hueUtils from 'utils/hueUtils';
 
-import { Snippet, STATUS as SNIPPET_STATUS } from 'apps/notebook2/snippet';
-
-const NOTEBOOK_MAPPING = {
-  ignore: [
-    'ace',
-    'aceEditor',
-    'aceMode',
-    'autocompleter',
-    'availableDatabases',
-    'availableSnippets',
-    'avoidClosing',
-    'canWrite',
-    'cleanedDateTimeMeta',
-    'cleanedMeta',
-    'cleanedNumericMeta',
-    'cleanedStringMeta',
-    'dependents',
-    'errorLoadingQueries',
-    'hasProperties',
-    'history',
-    'images',
-    'inFocus',
-    'parentNotebook',
-    'parentVm',
-    'queries',
-    'saveResultsModalVisible',
-    'selectedStatement',
-    'snippetImage',
-    'user',
-    'positionStatement',
-    'lastExecutedStatement',
-    'downloadResultViewModel'
-  ]
-};
-
-class Notebook {
+import Snippet, { STATUS as SNIPPET_STATUS } from 'apps/notebook2/snippet';
+import { notebookToContextJSON, notebookToJSON } from 'apps/notebook2/notebookSerde';
+
+export default class Notebook {
   constructor(vm, notebook) {
-    const self = this;
-
-    self.parentVm = vm;
-
-    self.id = ko.observable(notebook.id);
-    self.uuid = ko.observable(notebook.uuid || hueUtils.UUID());
-    self.name = ko.observable(notebook.name || 'My Notebook');
-    self.description = ko.observable(notebook.description || '');
-    self.type = ko.observable(notebook.type || 'notebook');
-    self.initialType = self.type().replace('query-', '');
-    self.coordinatorUuid = ko.observable(notebook.coordinatorUuid);
-    self.isHistory = ko.observable(!!notebook.is_history);
-    self.isManaged = ko.observable(!!notebook.isManaged);
-    self.parentSavedQueryUuid = ko.observable(notebook.parentSavedQueryUuid); // History parent
-    self.isSaved = ko.observable(!!notebook.isSaved);
-    self.canWrite = ko.observable(notebook.can_write !== false);
-    self.onSuccessUrl = ko.observable(notebook.onSuccessUrl);
-    self.pubSubUrl = ko.observable(notebook.pubSubUrl);
-    self.isPresentationModeDefault = ko.observable(!!notebook.isPresentationModeDefault);
-    self.isPresentationMode = ko.observable(false);
-    self.isPresentationModeInitialized = ko.observable(false);
-    self.isPresentationMode.subscribe(newValue => {
+    this.parentVm = vm;
+    this.id = ko.observable(notebook.id);
+    this.uuid = ko.observable(notebook.uuid || hueUtils.UUID());
+    this.name = ko.observable(notebook.name || '');
+    this.description = ko.observable(notebook.description || '');
+    this.type = ko.observable(notebook.type || 'notebook');
+    this.initialType = this.type().replace('query-', '');
+    this.coordinatorUuid = ko.observable(notebook.coordinatorUuid);
+    this.isHistory = ko.observable(!!notebook.is_history);
+    this.isManaged = ko.observable(!!notebook.isManaged);
+    this.parentSavedQueryUuid = ko.observable(notebook.parentSavedQueryUuid); // History parent
+    this.isSaved = ko.observable(!!notebook.isSaved);
+    this.canWrite = ko.observable(notebook.can_write !== false);
+    this.onSuccessUrl = ko.observable(notebook.onSuccessUrl);
+    this.pubSubUrl = ko.observable(notebook.pubSubUrl);
+    this.isPresentationModeDefault = ko.observable(!!notebook.isPresentationModeDefault);
+    this.isPresentationMode = ko.observable(false);
+    this.isPresentationModeInitialized = ko.observable(false);
+    this.isPresentationMode.subscribe(newValue => {
       if (!newValue) {
-        self.cancelExecutingAll();
+        this.cancelExecutingAll();
       }
       huePubSub.publish('editor.presentation.operate.toggle', newValue); // Problem with headers / row numbers redraw on full screen results
       vm.togglePresentationMode();
@@ -91,95 +56,95 @@ class Notebook {
         hueAnalytics.convert('editor', 'presentation');
       }
     });
-    self.presentationSnippets = ko.observable({});
-    self.isHidingCode = ko.observable(!!notebook.isHidingCode);
-
-    self.snippets = ko.observableArray();
-    self.selectedSnippet = ko.observable(vm.editorType()); // Aka selectedSnippetType
-    self.directoryUuid = ko.observable(notebook.directoryUuid);
-    self.dependents = komapping.fromJS(notebook.dependents || []);
-    self.dependentsCoordinator = ko.pureComputed(() =>
-      self.dependents().filter(doc => doc.type() === 'oozie-coordinator2' && doc.is_managed())
+    this.presentationSnippets = ko.observable({});
+    this.isHidingCode = ko.observable(!!notebook.isHidingCode);
+
+    this.snippets = ko.observableArray();
+    this.selectedSnippet = ko.observable(vm.editorType()); // Aka selectedSnippetType
+    this.directoryUuid = ko.observable(notebook.directoryUuid);
+    this.dependents = komapping.fromJS(notebook.dependents || []);
+    this.dependentsCoordinator = ko.pureComputed(() =>
+      this.dependents().filter(doc => doc.type() === 'oozie-coordinator2' && doc.is_managed())
     );
-    if (self.dependentsCoordinator().length > 0 && !self.coordinatorUuid()) {
-      self.coordinatorUuid(self.dependentsCoordinator()[0].uuid());
+    if (this.dependentsCoordinator().length > 0 && !this.coordinatorUuid()) {
+      this.coordinatorUuid(this.dependentsCoordinator()[0].uuid());
     }
-    self.history = ko.observableArray(
+    this.history = ko.observableArray(
       vm.selectedNotebook() &&
         vm.selectedNotebook().history().length > 0 &&
-        vm.selectedNotebook().history()[0].type === self.type()
+        vm.selectedNotebook().history()[0].type === this.type()
         ? vm.selectedNotebook().history()
         : []
     );
 
     // This is to keep the "Saved Query" tab selected when opening a doc from the left assist
     // TODO: Refactor code to reflect purpose
-    self.history.subscribe(val => {
+    this.history.subscribe(val => {
       if (
-        self.id() == null &&
-        val.length == 0 &&
-        self.historyFilter() === '' &&
+        this.id() == null &&
+        val.length === 0 &&
+        this.historyFilter() === '' &&
         !vm.isNotificationManager()
       ) {
-        self.snippets()[0].currentQueryTab('savedQueries');
+        this.snippets()[0].currentQueryTab('savedQueries');
       }
     });
 
-    self.historyFilter = ko.observable('');
-    self.historyFilterVisible = ko.observable(false);
-    self.historyFilter.extend({ rateLimit: { method: 'notifyWhenChangesStop', timeout: 900 } });
-    self.historyFilter.subscribe(() => {
-      if (self.historyCurrentPage() !== 1) {
-        self.historyCurrentPage(1);
+    this.historyFilter = ko.observable('');
+    this.historyFilterVisible = ko.observable(false);
+    this.historyFilter.extend({ rateLimit: { method: 'notifyWhenChangesStop', timeout: 900 } });
+    this.historyFilter.subscribe(() => {
+      if (this.historyCurrentPage() !== 1) {
+        this.historyCurrentPage(1);
       } else {
-        self.fetchHistory();
+        this.fetchHistory();
       }
     });
-    self.loadingHistory = ko.observable(self.history().length === 0);
-    self.historyInitialHeight = ko.observable(0).extend({ throttle: 1000 });
-    self.forceHistoryInitialHeight = ko.observable(false);
-    self.historyCurrentPage = ko.observable(
+    this.loadingHistory = ko.observable(this.history().length === 0);
+    this.historyInitialHeight = ko.observable(0).extend({ throttle: 1000 });
+    this.forceHistoryInitialHeight = ko.observable(false);
+    this.historyCurrentPage = ko.observable(
       vm.selectedNotebook() ? vm.selectedNotebook().historyCurrentPage() : 1
     );
-    self.historyCurrentPage.subscribe(() => {
-      self.fetchHistory();
+    this.historyCurrentPage.subscribe(() => {
+      this.fetchHistory();
     });
-    self.historyTotalPages = ko.observable(
+    this.historyTotalPages = ko.observable(
       vm.selectedNotebook() ? vm.selectedNotebook().historyTotalPages() : 1
     );
 
-    self.schedulerViewModel = null;
-    self.schedulerViewModelIsLoaded = ko.observable(false);
-    self.schedulerViewerViewModel = ko.observable();
-    self.isBatchable = ko.pureComputed(
-      () => self.snippets().length > 0 && self.snippets().every(snippet => snippet.isBatchable())
+    this.schedulerViewModel = null;
+    this.schedulerViewModelIsLoaded = ko.observable(false);
+    this.schedulerViewerViewModel = ko.observable();
+    this.isBatchable = ko.pureComputed(
+      () => this.snippets().length > 0 && this.snippets().every(snippet => snippet.isBatchable())
     );
 
-    self.isExecutingAll = ko.observable(!!notebook.isExecutingAll);
+    this.isExecutingAll = ko.observable(!!notebook.isExecutingAll);
 
-    self.executingAllIndex = ko.observable(notebook.executingAllIndex || 0);
+    this.executingAllIndex = ko.observable(notebook.executingAllIndex || 0);
 
-    self.retryModalConfirm = null;
-    self.retryModalCancel = null;
+    this.retryModalConfirm = null;
+    this.retryModalCancel = null;
 
-    self.avoidClosing = false;
+    this.avoidClosing = false;
 
-    self.canSave = vm.canSave;
+    this.canSave = vm.canSave;
 
-    self.unloaded = ko.observable(false);
-    self.updateHistoryFailed = false;
+    this.unloaded = ko.observable(false);
+    this.updateHistoryFailed = false;
 
-    self.viewSchedulerId = ko.observable(notebook.viewSchedulerId || '');
-    self.viewSchedulerId.subscribe(() => {
-      self.save();
+    this.viewSchedulerId = ko.observable(notebook.viewSchedulerId || '');
+    this.viewSchedulerId.subscribe(() => {
+      this.save();
     });
-    self.isSchedulerJobRunning = ko.observable();
-    self.loadingScheduler = ko.observable(false);
+    this.isSchedulerJobRunning = ko.observable();
+    this.loadingScheduler = ko.observable(false);
 
     // Init
     if (notebook.snippets) {
       notebook.snippets.forEach(snippet => {
-        self.addSnippet(snippet);
+        this.addSnippet(snippet);
       });
       if (
         typeof notebook.presentationSnippets != 'undefined' &&
@@ -188,16 +153,16 @@ class Notebook {
         // Load
         $.each(notebook.presentationSnippets, (key, snippet) => {
           snippet.status = 'ready'; // Protect from storm of check_statuses
-          const _snippet = new Snippet(vm, self, snippet);
+          const _snippet = new Snippet(vm, this, snippet);
           _snippet.init();
           _snippet.previousChartOptions = vm.getPreviousChartOptions(_snippet);
-          self.presentationSnippets()[key] = _snippet;
+          this.presentationSnippets()[key] = _snippet;
         });
       }
-      if (vm.editorMode() && self.history().length === 0) {
-        self.fetchHistory(() => {
-          self.updateHistory(['starting', 'running'], 30000);
-          self.updateHistory(['available'], 60000 * 5);
+      if (vm.editorMode() && this.history().length === 0) {
+        this.fetchHistory(() => {
+          this.updateHistory(['starting', 'running'], 30000);
+          this.updateHistory(['available'], 60000 * 5);
         });
       }
     }
@@ -205,7 +170,7 @@ class Notebook {
     huePubSub.subscribeOnce(
       'assist.db.panel.ready',
       () => {
-        if (self.type().indexOf('query') === 0) {
+        if (this.type().indexOf('query') === 0) {
           const whenDatabaseAvailable = function(snippet) {
             huePubSub.publish('assist.set.database', {
               source: snippet.type(),
@@ -236,10 +201,10 @@ class Notebook {
             }
           };
 
-          if (self.snippets().length === 1) {
-            whenSnippetAvailable(self.snippets()[0]);
+          if (this.snippets().length === 1) {
+            whenSnippetAvailable(this.snippets()[0]);
           } else {
-            const snippetsSub = self.snippets.subscribe(snippets => {
+            const snippetsSub = this.snippets.subscribe(snippets => {
               if (snippets.length === 1) {
                 whenSnippetAvailable(snippets[0]);
               }
@@ -255,37 +220,34 @@ class Notebook {
   }
 
   addSnippet(snippet) {
-    const self = this;
-    const newSnippet = new Snippet(self.parentVm, self, snippet);
-    self.snippets.push(newSnippet);
+    const newSnippet = new Snippet(this.parentVm, this, snippet);
+    this.snippets.push(newSnippet);
     newSnippet.init();
     return newSnippet;
   }
 
   cancelExecutingAll() {
-    const self = this;
-    const index = self.executingAllIndex();
-    if (self.isExecutingAll() && self.snippets()[index]) {
-      self.snippets()[index].cancel();
+    const index = this.executingAllIndex();
+    if (this.isExecutingAll() && this.snippets()[index]) {
+      this.snippets()[index].cancel();
     }
   }
 
   clearHistory() {
-    const self = this;
     hueAnalytics.log('notebook', 'clearHistory');
     apiHelper
       .clearNotebookHistory({
-        notebookJson: komapping.toJSON(self.getContext(), NOTEBOOK_MAPPING),
-        docType: self.selectedSnippet(),
-        isNotificationManager: self.parentVm.isNotificationManager()
+        notebookJson: notebookToContextJSON(this),
+        docType: this.selectedSnippet(),
+        isNotificationManager: this.parentVm.isNotificationManager()
       })
       .then(() => {
-        self.history.removeAll();
-        if (self.isHistory()) {
-          self.id(null);
-          self.uuid(hueUtils.UUID());
-          self.parentVm.changeURL(
-            self.parentVm.URLS.editor + '?type=' + self.parentVm.editorType()
+        this.history.removeAll();
+        if (this.isHistory()) {
+          this.id(null);
+          this.uuid(hueUtils.UUID());
+          this.parentVm.changeURL(
+            this.parentVm.URLS.editor + '?type=' + this.parentVm.editorType()
           );
         }
       })
@@ -298,54 +260,50 @@ class Notebook {
   }
 
   clearResults() {
-    const self = this;
-    self.snippets().forEach(snippet => {
+    this.snippets().forEach(snippet => {
       snippet.result.clear();
       snippet.status(SNIPPET_STATUS.ready);
     });
   }
 
   close() {
-    const self = this;
     hueAnalytics.log('notebook', 'close');
     apiHelper.closeNotebook({
-      notebookJson: komapping.toJSON(self, NOTEBOOK_MAPPING),
-      editorMode: self.parentVm.editorMode()
+      notebookJson: notebookToJSON(this),
+      editorMode: this.parentVm.editorMode()
     });
   }
 
   executeAll() {
-    const self = this;
-    if (self.isExecutingAll() || self.snippets().length === 0) {
+    if (this.isExecutingAll() || this.snippets().length === 0) {
       return;
     }
 
-    self.isExecutingAll(true);
-    self.executingAllIndex(0);
+    this.isExecutingAll(true);
+    this.executingAllIndex(0);
 
-    self.snippets()[self.executingAllIndex()].execute();
+    this.snippets()[this.executingAllIndex()].execute();
   }
 
   fetchHistory(callback) {
-    const self = this;
     const QUERIES_PER_PAGE = 50;
-    self.loadingHistory(true);
+    this.loadingHistory(true);
 
     $.get(
       '/notebook/api/get_history',
       {
-        doc_type: self.selectedSnippet(),
+        doc_type: this.selectedSnippet(),
         limit: QUERIES_PER_PAGE,
-        page: self.historyCurrentPage(),
-        doc_text: self.historyFilter(),
-        is_notification_manager: self.parentVm.isNotificationManager()
+        page: this.historyCurrentPage(),
+        doc_text: this.historyFilter(),
+        is_notification_manager: this.parentVm.isNotificationManager()
       },
       data => {
         const parsedHistory = [];
         if (data && data.history) {
           data.history.forEach(nbk => {
             parsedHistory.push(
-              self.makeHistoryRecord(
+              this.makeHistoryRecord(
                 nbk.absoluteUrl,
                 nbk.data.statement,
                 nbk.data.lastExecuted,
@@ -356,39 +314,25 @@ class Notebook {
             );
           });
         }
-        self.history(parsedHistory);
-        self.historyTotalPages(Math.ceil(data.count / QUERIES_PER_PAGE));
+        this.history(parsedHistory);
+        this.historyTotalPages(Math.ceil(data.count / QUERIES_PER_PAGE));
       }
     ).always(() => {
-      self.loadingHistory(false);
+      this.loadingHistory(false);
       if (callback) {
         callback();
       }
     });
   }
 
-  getContext() {
-    const self = this;
-    return {
-      id: self.id,
-      uuid: self.uuid,
-      parentSavedQueryUuid: self.parentSavedQueryUuid,
-      isSaved: self.isSaved,
-      type: self.type,
-      name: self.name
-    };
-  }
-
   getSnippets(type) {
-    const self = this;
-    return self.snippets().filter(snippet => snippet.type() === type);
+    return this.snippets().filter(snippet => snippet.type() === type);
   }
 
   loadScheduler() {
-    const self = this;
-    if (typeof self.parentVm.CoordinatorEditorViewModel !== 'undefined' && self.isBatchable()) {
+    if (typeof this.parentVm.CoordinatorEditorViewModel !== 'undefined' && this.isBatchable()) {
       let action;
-      if (self.coordinatorUuid()) {
+      if (this.coordinatorUuid()) {
         action = 'edit';
       } else {
         action = 'new';
@@ -400,8 +344,8 @@ class Notebook {
           '/scheduler/api/schedule/' + action + '/',
           {
             format: 'json',
-            document: self.uuid(),
-            coordinator: self.coordinatorUuid()
+            document: this.uuid(),
+            coordinator: this.coordinatorUuid()
           },
           data => {
             if ($('#schedulerEditor').length > 0) {
@@ -411,7 +355,7 @@ class Notebook {
                   const $schedulerEditor = $('#schedulerEditor');
                   $schedulerEditor.html(r);
 
-                  self.schedulerViewModel = new self.parentVm.CoordinatorEditorViewModel(
+                  this.schedulerViewModel = new this.parentVm.CoordinatorEditorViewModel(
                     data.coordinator,
                     data.credentials,
                     data.workflows,
@@ -419,7 +363,7 @@ class Notebook {
                   );
 
                   ko.cleanNode($schedulerEditor[0]);
-                  ko.applyBindings(self.schedulerViewModel, $schedulerEditor[0]);
+                  ko.applyBindings(this.schedulerViewModel, $schedulerEditor[0]);
                   $(document).off('showSubmitPopup');
                   $(document).on('showSubmitPopup', (event, data) => {
                     const $submitModalEditor = $('.submit-modal-editor');
@@ -437,14 +381,14 @@ class Notebook {
 
                   huePubSub.publish('render.jqcron');
 
-                  self.schedulerViewModel.coordinator.properties.cron_advanced.valueHasMutated(); // Update jsCron enabled status
-                  self.schedulerViewModel.coordinator.tracker().markCurrentStateAsClean();
-                  self.schedulerViewModel.isEditing(true);
+                  this.schedulerViewModel.coordinator.properties.cron_advanced.valueHasMutated(); // Update jsCron enabled status
+                  this.schedulerViewModel.coordinator.tracker().markCurrentStateAsClean();
+                  this.schedulerViewModel.isEditing(true);
 
-                  self.schedulerViewModelIsLoaded(true);
+                  this.schedulerViewModelIsLoaded(true);
 
                   if (action === 'new') {
-                    self.schedulerViewModel.coordinator.properties.document(self.uuid()); // Expected for triggering the display
+                    this.schedulerViewModel.coordinator.properties.document(this.uuid()); // Expected for triggering the display
                   }
                 }
               });
@@ -473,12 +417,11 @@ class Notebook {
   }
 
   newSnippet(type) {
-    const self = this;
     if (type) {
-      self.selectedSnippet(type);
+      this.selectedSnippet(type);
     }
-    const snippet = self.addSnippet({
-      type: self.selectedSnippet(),
+    const snippet = this.addSnippet({
+      type: this.selectedSnippet(),
       result: {}
     });
 
@@ -489,152 +432,127 @@ class Notebook {
       }
     }, 100);
 
-    hueAnalytics.log('notebook', 'add_snippet/' + (type ? type : self.selectedSnippet()));
+    hueAnalytics.log('notebook', 'add_snippet/' + (type ? type : this.selectedSnippet()));
     return snippet;
   }
 
   newSnippetAbove(id) {
-    const self = this;
-    self.newSnippet();
+    this.newSnippet();
     let idx = 0;
-    self.snippets().forEach((snippet, cnt) => {
+    this.snippets().forEach((snippet, cnt) => {
       if (snippet.id() === id) {
         idx = cnt;
       }
     });
-    self.snippets(self.snippets().move(self.snippets().length - 1, idx));
+    this.snippets(this.snippets().move(this.snippets().length - 1, idx));
   }
 
   nextHistoryPage() {
-    const self = this;
-    if (self.historyCurrentPage() < self.historyTotalPages()) {
-      self.historyCurrentPage(self.historyCurrentPage() + 1);
+    if (this.historyCurrentPage() < this.historyTotalPages()) {
+      this.historyCurrentPage(this.historyCurrentPage() + 1);
     }
   }
 
   prevHistoryPage() {
-    const self = this;
-    if (self.historyCurrentPage() !== 1) {
-      self.historyCurrentPage(self.historyCurrentPage() - 1);
+    if (this.historyCurrentPage() !== 1) {
+      this.historyCurrentPage(this.historyCurrentPage() - 1);
     }
   }
 
-  save(callback) {
-    const self = this;
+  async save(callback) {
     hueAnalytics.log('notebook', 'save');
 
-    // Remove the result data from the snippets
-    // Also do it for presentation mode
-    const cp = komapping.toJS(self, NOTEBOOK_MAPPING);
-    cp.snippets
-      .concat(Object.keys(cp.presentationSnippets).map(key => cp.presentationSnippets[key]))
-      .forEach(snippet => {
-        snippet.result.data.length = 0; // snippet.result.clear() does not work for some reason
-        snippet.result.meta.length = 0;
-        snippet.result.logs = '';
-        snippet.result.fetchedOnce = false;
-        snippet.progress = 0; // Remove progress
-        snippet.jobs.length = 0;
-      });
-    if (cp.schedulerViewModel) {
-      cp.schedulerViewModel.availableTimezones = [];
-    }
     const editorMode =
-      self.parentVm.editorMode() ||
-      (self.isPresentationMode() && self.parentVm.editorType() !== 'notebook'); // Editor should not convert to Notebook in presentation mode
+      this.parentVm.editorMode() ||
+      (this.isPresentationMode() && this.parentVm.editorType() !== 'notebook'); // Editor should not convert to Notebook in presentation mode
 
-    apiHelper
-      .saveNotebook({
-        notebookJson: komapping.toJSON(cp, NOTEBOOK_MAPPING),
+    try {
+      const data = await apiHelper.saveNotebook({
+        notebookJson: notebookToJSON(this),
         editorMode: editorMode
-      })
-      .then(data => {
-        if (data.status === 0) {
-          self.id(data.id);
-          self.isSaved(true);
-          const wasHistory = self.isHistory();
-          self.isHistory(false);
-          $(document).trigger('info', data.message);
-          if (editorMode) {
-            if (!data.save_as) {
-              const existingQuery = self
-                .snippets()[0]
-                .queries()
-                .filter(item => item.uuid() === data.uuid);
-              if (existingQuery.length > 0) {
-                existingQuery[0].name(data.name);
-                existingQuery[0].description(data.description);
-                existingQuery[0].last_modified(data.last_modified);
-              }
-            } else if (self.snippets()[0].queries().length > 0) {
-              // Saved queries tab already loaded
-              self.snippets()[0].queries.unshift(komapping.fromJS(data));
-            }
+      });
 
-            if (self.coordinatorUuid() && self.schedulerViewModel) {
-              self.saveScheduler();
-              self.schedulerViewModel.coordinator.refreshParameters();
-            }
-            if (wasHistory || data.save_as) {
-              self.loadScheduler();
+      if (data.status === 0) {
+        this.id(data.id);
+        this.isSaved(true);
+        const wasHistory = this.isHistory();
+        this.isHistory(false);
+        $(document).trigger('info', data.message);
+        if (editorMode) {
+          if (!data.save_as) {
+            const existingQuery = this.snippets()[0]
+              .queries()
+              .filter(item => item.uuid() === data.uuid);
+            if (existingQuery.length > 0) {
+              existingQuery[0].name(data.name);
+              existingQuery[0].description(data.description);
+              existingQuery[0].last_modified(data.last_modified);
             }
+          } else if (this.snippets()[0].queries().length > 0) {
+            // Saved queries tab already loaded
+            this.snippets()[0].queries.unshift(komapping.fromJS(data));
+          }
 
-            if (
-              self.snippets()[0].downloadResultViewModel &&
-              self
-                .snippets()[0]
-                .downloadResultViewModel()
-                .saveTarget() === 'dashboard'
-            ) {
-              huePubSub.publish(
-                'open.link',
-                self.parentVm.URLS.report +
-                  '&uuid=' +
-                  data.uuid +
-                  '&statement=' +
-                  self.snippets()[0].result.handle().statement_id
-              );
-            } else {
-              self.parentVm.changeURL(self.parentVm.URLS.editor + '?editor=' + data.id);
-            }
-          } else {
-            self.parentVm.changeURL(self.parentVm.URLS.notebook + '?notebook=' + data.id);
+          if (this.coordinatorUuid() && this.schedulerViewModel) {
+            this.saveScheduler();
+            this.schedulerViewModel.coordinator.refreshParameters();
+          }
+          if (wasHistory || data.save_as) {
+            this.loadScheduler();
           }
-          if (typeof callback == 'function') {
-            callback();
+
+          if (
+            this.snippets()[0].downloadResultViewModel &&
+            this.snippets()[0]
+              .downloadResultViewModel()
+              .saveTarget() === 'dashboard'
+          ) {
+            huePubSub.publish(
+              'open.link',
+              this.parentVm.URLS.report +
+                '&uuid=' +
+                data.uuid +
+                '&statement=' +
+                this.snippets()[0].result.handle().statement_id
+            );
+          } else {
+            this.parentVm.changeURL(this.parentVm.URLS.editor + '?editor=' + data.id);
           }
         } else {
-          $(document).trigger('error', data.message);
+          this.parentVm.changeURL(this.parentVm.URLS.notebook + '?notebook=' + data.id);
         }
-      })
-      .fail(xhr => {
-        if (xhr.status !== 502) {
-          $(document).trigger('error', xhr.responseText);
+        if (typeof callback == 'function') {
+          callback();
         }
-      });
+      } else {
+        $(document).trigger('error', data.message);
+      }
+    } catch (err) {
+      if (err && err.status !== 502) {
+        $(document).trigger('error', err.responseText);
+      }
+    }
   }
 
   saveScheduler() {
-    const self = this;
     if (
-      self.isBatchable() &&
-      (!self.coordinatorUuid() || self.schedulerViewModel.coordinator.isDirty())
+      this.isBatchable() &&
+      (!this.coordinatorUuid() || this.schedulerViewModel.coordinator.isDirty())
     ) {
-      self.schedulerViewModel.coordinator.isManaged(true);
-      self.schedulerViewModel.coordinator.properties.document(self.uuid());
-      self.schedulerViewModel.save(data => {
-        if (!self.coordinatorUuid()) {
-          self.coordinatorUuid(data.uuid);
-          self.save();
+      this.schedulerViewModel.coordinator.isManaged(true);
+      this.schedulerViewModel.coordinator.properties.document(this.uuid());
+      this.schedulerViewModel.save(data => {
+        if (!this.coordinatorUuid()) {
+          this.coordinatorUuid(data.uuid);
+          this.save();
         }
       });
     }
   }
 
   showSubmitPopup() {
-    const self = this;
     $.get(
-      '/scheduler/api/submit/' + self.coordinatorUuid(),
+      '/scheduler/api/submit/' + this.coordinatorUuid(),
       {
         format: 'json'
       },
@@ -649,10 +567,9 @@ class Notebook {
   }
 
   unload() {
-    const self = this;
-    self.unloaded(true);
+    this.unloaded(true);
     let currentQueries = null;
-    self.snippets().forEach(snippet => {
+    this.snippets().forEach(snippet => {
       if (snippet.checkStatusTimeout != null) {
         clearTimeout(snippet.checkStatusTimeout);
         snippet.checkStatusTimeout = null;
@@ -665,9 +582,7 @@ class Notebook {
   }
 
   updateHistory(statuses, interval) {
-    const self = this;
-    let items = self
-      .history()
+    let items = this.history()
       .filter(item => statuses.indexOf(item.status()) !== -1)
       .slice(0, 25);
 
@@ -687,7 +602,7 @@ class Notebook {
         })
         .fail(() => {
           items = [];
-          self.updateHistoryFailed = true;
+          this.updateHistoryFailed = true;
           console.warn('Lost connectivity to the Hue history refresh backend.');
         })
         .always(() => {
@@ -695,9 +610,9 @@ class Notebook {
             window.setTimeout(() => {
               updateHistoryCall(items.pop());
             }, 1000);
-          } else if (!self.updateHistoryFailed) {
+          } else if (!this.updateHistoryFailed) {
             window.setTimeout(() => {
-              self.updateHistory(statuses, interval);
+              this.updateHistory(statuses, interval);
             }, interval);
           }
         });
@@ -705,12 +620,10 @@ class Notebook {
 
     if (items.length > 0) {
       updateHistoryCall(items.pop());
-    } else if (!self.updateHistoryFailed) {
+    } else if (!this.updateHistoryFailed) {
       window.setTimeout(() => {
-        self.updateHistory(statuses, interval);
+        this.updateHistory(statuses, interval);
       }, interval);
     }
   }
 }
-
-export { Notebook, NOTEBOOK_MAPPING };

+ 127 - 0
desktop/core/src/desktop/js/apps/notebook2/notebookSerde.js

@@ -0,0 +1,127 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import komapping from 'knockout.mapping';
+
+export const snippetToContextJSON = snippet =>
+  JSON.stringify({
+    id: snippet.id(),
+    type: snippet.type(),
+    status: snippet.status(),
+    statementType: snippet.statementType(),
+    statement: snippet.statement(),
+    aceCursorPosition: snippet.aceCursorPosition(),
+    statementPath: snippet.statementPath(),
+    associatedDocumentUuid: snippet.associatedDocumentUuid(),
+    properties: komapping.toJS(snippet.properties), // TODO: Drop komapping
+    result: {}, // TODO: Moved to executor but backend requires it
+    database: snippet.database(),
+    compute: snippet.compute(),
+    wasBatchExecuted: snippet.wasBatchExecuted()
+  });
+
+export const notebookToContextJSON = notebook =>
+  JSON.stringify({
+    id: notebook.id(),
+    isSaved: notebook.isSaved(),
+    name: notebook.name(),
+    parentSavedQueryUuid: notebook.parentSavedQueryUuid(),
+    type: notebook.type(),
+    uuid: notebook.uuid()
+  });
+
+export const notebookToJSON = notebook =>
+  JSON.stringify({
+    coordinatorUuid: notebook.coordinatorUuid(),
+    description: notebook.description(),
+    directoryUuid: notebook.directoryUuid(),
+    executingAllIndex: notebook.executingAllIndex(),
+    id: notebook.id(),
+    isExecutingAll: notebook.isExecutingAll(),
+    isHidingCode: notebook.isHidingCode(),
+    isHistory: notebook.isHistory(),
+    isManaged: notebook.isManaged(),
+    isPresentationModeDefault: notebook.isPresentationModeDefault(),
+    isSaved: notebook.isSaved(),
+    name: notebook.name(),
+    onSuccessUrl: notebook.onSuccessUrl(),
+    parentSavedQueryUuid: notebook.parentSavedQueryUuid(),
+    presentationSnippets: notebook.presentationSnippets(),
+    pubSubUrl: notebook.pubSubUrl(),
+    result: {}, // TODO: Moved to executor but backend requires it
+    sessions: [], // TODO: Moved to executor but backend requires it
+    snippets: notebook.snippets().map(snippet => ({
+      aceCursorPosition: snippet.aceCursorPosition(),
+      aceSize: snippet.aceSize(),
+      associatedDocumentUuid: snippet.associatedDocumentUuid(),
+      chartData: snippet.chartData(),
+      chartLimit: snippet.chartLimit(),
+      chartMapHeat: snippet.chartMapHeat(),
+      chartMapLabel: snippet.chartMapLabel(),
+      chartMapType: snippet.chartMapType(),
+      chartScatterGroup: snippet.chartScatterGroup(),
+      chartScatterSize: snippet.chartScatterSize(),
+      chartScope: snippet.chartScope(),
+      chartSorting: snippet.chartSorting(),
+      chartTimelineType: snippet.chartTimelineType(),
+      chartType: snippet.chartType(),
+      chartX: snippet.chartX(),
+      chartXPivot: snippet.chartXPivot(),
+      chartYMulti: snippet.chartYMulti(),
+      chartYSingle: snippet.chartYSingle(),
+      compute: snippet.compute(),
+      currentQueryTab: snippet.currentQueryTab(),
+      database: snippet.database(),
+      id: snippet.id(),
+      is_redacted: snippet.is_redacted(),
+      isResultSettingsVisible: snippet.isResultSettingsVisible(),
+      lastAceSelectionRowOffset: snippet.lastAceSelectionRowOffset(),
+      lastExecuted: snippet.lastExecuted(),
+      name: snippet.name(),
+      namespace: snippet.namespace(),
+      pinnedContextTabs: snippet.pinnedContextTabs(),
+      properties: komapping.toJS(snippet.properties), // TODO: Drop komapping
+      // result: ...,
+      settingsVisible: snippet.settingsVisible(),
+      // schedulerViewModel: ?
+      showChart: snippet.showChart(),
+      showGrid: snippet.showGrid(),
+      showLogs: snippet.showLogs(),
+      statement_raw: snippet.statement_raw(),
+      statementPath: snippet.statementPath(),
+      statementType: snippet.statementType(),
+      status: snippet.status(),
+      type: snippet.type(),
+      variables: snippet.variables().map(variable => ({
+        meta: variable.meta && {
+          options: variable.meta.options && variable.meta.options(), // TODO: Map?
+          placeHolder: variable.meta.placeHolder && variable.meta.placeHolder(),
+          type: variable.meta.type && variable.meta.type()
+        },
+        name: variable.name(),
+        path: variable.path(),
+        sample: variable.sample(),
+        sampleUser: variable.sampleUser(),
+        step: variable.step(),
+        type: variable.type(),
+        value: variable.value()
+      })),
+      wasBatchExecuted: snippet.wasBatchExecuted()
+    })),
+    type: notebook.type(),
+    uuid: notebook.uuid(),
+    viewSchedulerId: notebook.viewSchedulerId()
+  });

File diff suppressed because it is too large
+ 307 - 310
desktop/core/src/desktop/js/apps/notebook2/snippet.js


+ 76 - 0
desktop/core/src/desktop/js/apps/notebook2/spec/notebookSerdeSpec.js

@@ -0,0 +1,76 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { notebookToContextJSON, notebookToJSON, snippetToContextJSON } from '../notebookSerde';
+import Notebook from '../notebook';
+import sessionManager from 'apps/notebook2/execution/sessionManager';
+
+describe('notebookSerde.js', () => {
+  const viewModel = {
+    editorType: () => 'notebook',
+    selectedNotebook: () => undefined,
+    availableSnippets: () => ({}),
+    editorMode: () => false,
+    getSnippetViewSettings: () => ({ sqlDialect: true })
+  };
+
+  window.HUE_CHARTS = {
+    TYPES: {
+      BARCHART: 'barchart'
+    }
+  };
+
+  beforeEach(() => {
+    spyOn(sessionManager, 'getSession').and.callFake(() => Promise.resolve());
+  });
+
+  it('should serialize a notebook to JSON', async () => {
+    const notebook = new Notebook(viewModel, {});
+    notebook.addSnippet({ type: 'hive' });
+    notebook.addSnippet({ type: 'impala' });
+
+    const notebookJSON = notebookToJSON(notebook);
+
+    const notebookRaw = JSON.parse(notebookJSON);
+
+    expect(notebookRaw.snippets.length).toEqual(2);
+    expect(notebookRaw.snippets[0].type).toEqual('hive');
+    expect(notebookRaw.snippets[1].type).toEqual('impala');
+  });
+
+  it('should serialize a notebook context to JSON', async () => {
+    const notebook = new Notebook(viewModel, {});
+    notebook.addSnippet({ type: 'hive' });
+
+    const notebookContextJSON = notebookToContextJSON(notebook);
+
+    const notebookContextRaw = JSON.parse(notebookContextJSON);
+
+    expect(notebookContextRaw.id).toEqual(notebook.id());
+  });
+
+  it('should serialize a snippet context to JSON', async () => {
+    const notebook = new Notebook(viewModel, {});
+    const snippet = notebook.addSnippet({ type: 'hive' });
+
+    const snippetContextJSON = snippetToContextJSON(snippet);
+
+    const snippetContextRaw = JSON.parse(snippetContextJSON);
+
+    expect(snippetContextRaw.id).toEqual(snippet.id());
+    expect(snippetContextRaw.type).toEqual('hive');
+  });
+});

+ 3 - 0
desktop/core/src/desktop/js/spec/globalJsConstants.js

@@ -14,6 +14,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+// Ensure singletons
+import 'apps/notebook2/execution/sessionManager';
+
 const globalVars = {
   LOGGED_USERNAME: 'foo',
   CACHEABLE_TTL: 1,

Some files were not shown because too many files changed in this diff