Selaa lähdekoodia

HUE-8768 [editor] Improve notebook and view model structure for notebook 2

Johan Ahlen 6 vuotta sitten
vanhempi
commit
4cd4f696c8

+ 2 - 2
desktop/core/src/desktop/js/apps/notebook/editorViewModel.js

@@ -463,7 +463,7 @@ class EditorViewModel {
             snippet.result.statement_range.valueHasMutated();
           }
 
-          snippet.previousChartOptions = self._getPreviousChartOptions(snippet);
+          snippet.previousChartOptions = self.getPreviousChartOptions(snippet);
         });
 
         if (notebook.snippets()[0].result.data().length > 0) {
@@ -485,7 +485,7 @@ class EditorViewModel {
       huePubSub.publish('recalculate.name.description.width');
     };
 
-    self._getPreviousChartOptions = function(snippet) {
+    self.getPreviousChartOptions = function(snippet) {
       return {
         chartLimit:
           typeof snippet.chartLimit() !== 'undefined'

+ 3 - 3
desktop/core/src/desktop/js/apps/notebook/notebook.js

@@ -637,7 +637,7 @@ class Notebook {
           if (data && data.history) {
             data.history.forEach(nbk => {
               parsedHistory.push(
-                self._makeHistoryRecord(
+                self.makeHistoryRecord(
                   nbk.absoluteUrl,
                   nbk.data.statement,
                   nbk.data.lastExecuted,
@@ -719,7 +719,7 @@ class Notebook {
       }
     };
 
-    self._makeHistoryRecord = function(url, statement, lastExecuted, status, name, uuid) {
+    self.makeHistoryRecord = function(url, statement, lastExecuted, status, name, uuid) {
       return komapping.fromJS({
         url: url,
         query: statement.substring(0, 1000) + (statement.length > 1000 ? '...' : ''),
@@ -887,7 +887,7 @@ class Notebook {
           snippet.status = 'ready'; // Protect from storm of check_statuses
           const _snippet = new Snippet(vm, self, snippet);
           _snippet.init();
-          _snippet.previousChartOptions = vm._getPreviousChartOptions(_snippet);
+          _snippet.previousChartOptions = vm.getPreviousChartOptions(_snippet);
           self.presentationSnippets()[key] = _snippet;
         });
       }

+ 2 - 2
desktop/core/src/desktop/js/apps/notebook/snippet.js

@@ -1720,7 +1720,7 @@ class Snippet {
         self.lastAceSelectionRowOffset(Math.min(selectionRange.start.row, selectionRange.end.row));
       }
 
-      self.previousChartOptions = vm._getPreviousChartOptions(self);
+      self.previousChartOptions = vm.getPreviousChartOptions(self);
       $(document).trigger('executeStarted', { vm: vm, snippet: self });
       self.lastExecuted(now);
       $('.jHueNotify').remove();
@@ -1826,7 +1826,7 @@ class Snippet {
                 }
               } else {
                 notebook.history.unshift(
-                  notebook._makeHistoryRecord(
+                  notebook.makeHistoryRecord(
                     undefined,
                     data.handle.statement,
                     self.lastExecuted(),

+ 378 - 387
desktop/core/src/desktop/js/apps/notebook2/editorViewModel.js

@@ -24,14 +24,19 @@ import huePubSub from 'utils/huePubSub';
 import hueUtils from 'utils/hueUtils';
 
 import { Notebook } from 'apps/notebook2/notebook';
-import Snippet from 'apps/notebook2/snippet';
+import { Snippet } from 'apps/notebook2/snippet';
 
 class EditorViewModel {
-  constructor(editor_id, notebooks, options, CoordinatorEditorViewModel, RunningCoordinatorModel) {
+  constructor(editorId, notebooks, options, CoordinatorEditorViewModel, RunningCoordinatorModel) {
     const self = this;
 
     console.log('Notebook 2 enabled.');
 
+    self.editorId = editorId;
+    self.sessionProperties = options.session_properties || [];
+    self.snippetViewSettings = options.snippetViewSettings;
+    self.notebooks = notebooks;
+
     self.URLS = {
       editor: '/hue/editor',
       notebook: '/hue/notebook',
@@ -43,111 +48,44 @@ class EditorViewModel {
     self.userId = options.userId;
     self.suffix = options.suffix;
     self.isMobile = ko.observable(options.mobile);
-    self.isNotificationManager = ko.observable(options.is_notification_manager || false);
+    self.isNotificationManager = ko.observable(!!options.is_notification_manager);
     self.editorType = ko.observable(options.editor_type);
     self.editorType.subscribe(newVal => {
-      self.editorMode(newVal != 'notebook');
+      self.editorMode(newVal !== 'notebook');
       hueUtils.changeURLParameter('type', newVal);
       if (self.editorMode()) {
         self.selectedNotebook().fetchHistory(); // Js error if notebook did not have snippets
       }
     });
     self.preEditorTogglingSnippet = ko.observable();
-    self.toggleEditorMode = function() {
-      const _notebook = self.selectedNotebook();
-      const _newSnippets = [];
-
-      if (self.editorType() != 'notebook') {
-        self.editorType('notebook');
-        self.preEditorTogglingSnippet(_notebook.snippets()[0]);
-        const _variables = _notebook.snippets()[0].variables();
-        const _statementKeys = [];
-        // Split statements
-        _notebook.type('notebook');
-        _notebook
-          .snippets()[0]
-          .statementsList()
-          .forEach(sql_statement => {
-            let _snippet;
-            if (sql_statement.hashCode() in _notebook.presentationSnippets()) {
-              _snippet = _notebook.presentationSnippets()[sql_statement.hashCode()]; // Persist result
-              _snippet.variables(_variables);
-            } else {
-              const _title = [];
-              const _statement = [];
-              sql_statement
-                .trim()
-                .split('\n')
-                .forEach(line => {
-                  if (line.trim().startsWith('--') && _statement.length === 0) {
-                    _title.push(line.substr(2));
-                  } else {
-                    _statement.push(line);
-                  }
-                });
-              _snippet = new Snippet(self, _notebook, {
-                type: _notebook.initialType,
-                statement_raw: _statement.join('\n'),
-                result: {},
-                name: _title.join('\n'),
-                variables: komapping.toJS(_variables)
-              });
-              _snippet.variables = _notebook.snippets()[0].variables;
-              _snippet.init();
-              _notebook.presentationSnippets()[sql_statement.hashCode()] = _snippet;
-            }
-            _statementKeys.push(sql_statement.hashCode());
-            _newSnippets.push(_snippet);
-          });
-        $.each(_notebook.presentationSnippets(), key => {
-          // Dead statements
-          if (!key in _statementKeys) {
-            delete _notebook.presentationSnippets()[key];
-          }
-        });
-      } else {
-        self.editorType(_notebook.initialType);
-        // Revert to one statement
-        _newSnippets.push(self.preEditorTogglingSnippet());
-        _notebook.type('query-' + _notebook.initialType);
-      }
-      _notebook.snippets(_newSnippets);
-      _newSnippets.forEach(snippet => {
-        huePubSub.publish('editor.redraw.data', { snippet: snippet });
-      });
-    };
-    self.togglePresentationMode = function() {
-      if (self.selectedNotebook().initialType !== 'notebook') {
-        self.toggleEditorMode();
-      }
-    };
+
     self.editorTypeTitle = ko.pureComputed(() => {
-      const foundInterpreter = $.grep(options.languages, interpreter => {
-        return interpreter.type === self.editorType();
-      });
-      return foundInterpreter.length > 0 ? foundInterpreter[0].name : self.editorType();
+      const foundInterpreters = options.languages.filter(
+        interpreter => interpreter.type === self.editorType()
+      );
+      return foundInterpreters.length > 0 ? foundInterpreters[0].name : self.editorType();
     });
+
     self.autocompleteTimeout = options.autocompleteTimeout;
     self.selectedNotebook = ko.observable();
 
     self.combinedContent = ko.observable();
-    self.isPresentationModeEnabled = ko.pureComputed(() => {
-      return (
+    self.isPresentationModeEnabled = ko.pureComputed(
+      () =>
         self.selectedNotebook() &&
         self.selectedNotebook().snippets().length === 1 &&
         self
           .selectedNotebook()
           .snippets()[0]
           .isSqlDialect()
-      );
-    });
+    );
     self.isResultFullScreenMode = ko.observable(false);
-    self.isPresentationMode = ko.computed(() => {
-      return self.selectedNotebook() && self.selectedNotebook().isPresentationMode();
-    });
-    self.isHidingCode = ko.computed(() => {
-      return self.selectedNotebook() && self.selectedNotebook().isHidingCode();
-    });
+    self.isPresentationMode = ko.pureComputed(
+      () => self.selectedNotebook() && self.selectedNotebook().isPresentationMode()
+    );
+    self.isHidingCode = ko.pureComputed(
+      () => self.selectedNotebook() && self.selectedNotebook().isHidingCode()
+    );
     self.successUrl = ko.observable(options.success_url); // Deprecated
     self.isOptimizerEnabled = ko.observable(options.is_optimizer_enabled);
     self.isNavigatorEnabled = ko.observable(options.is_navigator_enabled);
@@ -155,15 +93,14 @@ class EditorViewModel {
     self.CoordinatorEditorViewModel = CoordinatorEditorViewModel;
     self.RunningCoordinatorModel = RunningCoordinatorModel;
 
-    self.canSave = ko.computed(() => {
-      // Saved query or history but history coming from a saved query
-      return (
+    // Saved query or history but history coming from a saved query
+    self.canSave = ko.pureComputed(
+      () =>
         self.selectedNotebook() &&
         self.selectedNotebook().canWrite() &&
         (self.selectedNotebook().isSaved() ||
           (self.selectedNotebook().isHistory() && self.selectedNotebook().parentSavedQueryUuid()))
-      );
-    });
+    );
 
     self.ChartTransformers = ChartTransformers;
 
@@ -171,14 +108,14 @@ class EditorViewModel {
     self.sqlSourceTypes = [];
     self.availableLanguages = [];
 
-    if (options.languages && options.snippetViewSettings) {
-      $.each(options.languages, (idx, language) => {
+    if (options.languages && self.snippetViewSettings) {
+      options.languages.forEach(language => {
         self.availableLanguages.push({
           type: language.type,
           name: language.name,
           interface: language.interface
         });
-        const viewSettings = options.snippetViewSettings[language.type];
+        const viewSettings = self.snippetViewSettings[language.type];
         if (viewSettings && viewSettings.sqlDialect) {
           self.sqlSourceTypes.push({
             type: language.type,
@@ -188,40 +125,19 @@ class EditorViewModel {
       });
     }
 
-    const sqlSourceTypes = $.grep(self.sqlSourceTypes, language => {
-      return language.type == self.editorType();
-    });
+    const sqlSourceTypes = self.sqlSourceTypes.filter(
+      language => language.type === self.editorType()
+    );
     if (sqlSourceTypes.length > 0) {
       self.activeSqlSourceType = sqlSourceTypes[0].type;
     } else {
       self.activeSqlSourceType = null;
     }
 
-    self.displayCombinedContent = function() {
-      if (!self.selectedNotebook()) {
-        self.combinedContent('');
-      } else {
-        let statements = '';
-        $.each(self.selectedNotebook().snippets(), (index, snippet) => {
-          if (snippet.statement()) {
-            if (statements) {
-              statements += '\n\n';
-            }
-            statements += snippet.statement();
-          }
-        });
-        self.combinedContent(statements);
-      }
-      $('#combinedContentModal' + self.suffix).modal('show');
-    };
-
     self.isEditing = ko.observable(false);
     self.isEditing.subscribe(() => {
       $(document).trigger('editingToggled');
     });
-    self.toggleEditing = function() {
-      self.isEditing(!self.isEditing());
-    };
 
     self.authSessionUsername = ko.observable(); // UI popup
     self.authSessionPassword = ko.observable();
@@ -230,24 +146,6 @@ class EditorViewModel {
 
     self.removeSnippetConfirmation = ko.observable();
 
-    self.removeSnippet = function(notebook, snippet) {
-      let hasContent = snippet.statement_raw().length > 0;
-      if (!hasContent) {
-        $.each(snippet.properties(), (key, value) => {
-          hasContent = hasContent || (ko.isObservable(value) && value().length > 0);
-        });
-      }
-      if (hasContent) {
-        self.removeSnippetConfirmation({ notebook: notebook, snippet: snippet });
-        $('#removeSnippetModal' + self.suffix).modal('show');
-      } else {
-        notebook.snippets.remove(snippet);
-        window.setTimeout(() => {
-          $(document).trigger('editorSizeChanged');
-        }, 100);
-      }
-    };
-
     self.assistAvailable = ko.observable(options.assistAvailable);
 
     self.assistWithoutStorage = ko.observable(false);
@@ -374,291 +272,384 @@ class EditorViewModel {
 
     self.editorMode = ko.observable(options.mode === 'editor');
 
-    self.getSnippetViewSettings = function(snippetType) {
-      if (options.snippetViewSettings[snippetType]) {
-        return options.snippetViewSettings[snippetType];
-      }
-      return options.snippetViewSettings.default;
-    };
+    // Only Spark
+    // Could filter out the ones already selected + yarn only or not
+    self.availableSessionProperties = ko.pureComputed(() =>
+      self.sessionProperties.filter(item => item.name !== '')
+    );
+  }
 
-    self.availableSessionProperties = ko.computed(() => {
-      // Only Spark
-      return ko.utils.arrayFilter(options.session_properties, item => {
-        return item.name != ''; // Could filter out the ones already selected + yarn only or not
-      });
-    });
-    self.getSessionProperties = function(name) {
-      let _prop = null;
-      $.each(options.session_properties, (index, prop) => {
-        if (prop.name == name) {
-          _prop = prop;
-          return;
-        }
-      });
-      return _prop;
-    };
+  changeURL(url) {
+    const self = this;
+    if (!self.isNotificationManager()) {
+      hueUtils.changeURL(url);
+    }
+  }
 
-    self.getSnippetName = function(snippetType) {
-      const availableSnippets = self.availableSnippets();
-      for (let i = 0; i < availableSnippets.length; i++) {
-        if (availableSnippets[i].type() === snippetType) {
-          return availableSnippets[i].name();
-        }
-      }
-      return '';
-    };
+  displayCombinedContent() {
+    const self = this;
+    if (!self.selectedNotebook()) {
+      self.combinedContent('');
+    } else {
+      let statements = '';
+      self
+        .selectedNotebook()
+        .snippets()
+        .forEach(snippet => {
+          if (snippet.statement()) {
+            if (statements) {
+              statements += '\n\n';
+            }
+            statements += snippet.statement();
+          }
+        });
+      self.combinedContent(statements);
+    }
+    $('#combinedContentModal' + self.suffix).modal('show');
+  }
 
-    self.changeURL = function(url) {
-      if (!self.isNotificationManager()) {
-        hueUtils.changeURL(url);
-      }
+  getPreviousChartOptions(snippet) {
+    return {
+      chartLimit: snippet.chartLimit() || snippet.previousChartOptions.chartLimit,
+      chartX: snippet.chartX() || snippet.previousChartOptions.chartX,
+      chartXPivot: snippet.chartXPivot() || snippet.previousChartOptions.chartXPivot,
+      chartYSingle: snippet.chartYSingle() || snippet.previousChartOptions.chartYSingle,
+      chartMapType: snippet.chartMapType() || snippet.previousChartOptions.chartMapType,
+      chartMapLabel: snippet.chartMapLabel() || snippet.previousChartOptions.chartMapLabel,
+      chartMapHeat: snippet.chartMapHeat() || snippet.previousChartOptions.chartMapHeat,
+      chartYMulti: snippet.chartYMulti() || snippet.previousChartOptions.chartYMulti,
+      chartScope: snippet.chartScope() || snippet.previousChartOptions.chartScope,
+      chartTimelineType:
+        snippet.chartTimelineType() || snippet.previousChartOptions.chartTimelineType,
+      chartSorting: snippet.chartSorting() || snippet.previousChartOptions.chartSorting,
+      chartScatterGroup:
+        snippet.chartScatterGroup() || snippet.previousChartOptions.chartScatterGroup,
+      chartScatterSize: snippet.chartScatterSize() || snippet.previousChartOptions.chartScatterSize
     };
+  }
 
-    self.init = function() {
-      if (editor_id) {
-        self.openNotebook(editor_id);
-      } else if (window.location.getParameter('editor') !== '') {
-        self.openNotebook(window.location.getParameter('editor'));
-      } else if (notebooks.length > 0) {
-        self.loadNotebook(notebooks[0]); // Old way of loading json for /browse
-      } else if (window.location.getParameter('type') !== '') {
-        self.newNotebook(window.location.getParameter('type'));
-      } else {
-        self.newNotebook();
+  getSessionProperties(name) {
+    const self = this;
+    let result = null;
+    self.sessionProperties.some(prop => {
+      if (prop.name === name) {
+        result = prop;
+        return true;
       }
-    };
+    });
+    return result;
+  }
 
-    self.loadNotebook = function(notebookRaw, queryTab) {
-      let currentQueries;
-      if (self.selectedNotebook() != null) {
-        currentQueries = self.selectedNotebook().unload();
+  getSnippetName(snippetType) {
+    const self = this;
+    const availableSnippets = self.availableSnippets();
+    for (let i = 0; i < availableSnippets.length; i++) {
+      if (availableSnippets[i].type() === snippetType) {
+        return availableSnippets[i].name();
       }
+    }
+    return '';
+  }
 
-      const notebook = new Notebook(self, notebookRaw);
+  getSnippetViewSettings(snippetType) {
+    const self = this;
+    if (self.snippetViewSettings[snippetType]) {
+      return self.snippetViewSettings[snippetType];
+    }
+    return self.snippetViewSettings.default;
+  }
 
-      if (notebook.snippets().length > 0) {
-        huePubSub.publish('detach.scrolls', notebook.snippets()[0]);
-        notebook.selectedSnippet(notebook.snippets()[notebook.snippets().length - 1].type());
-        if (currentQueries != null) {
-          notebook.snippets()[0].queries(currentQueries);
-        }
-        notebook.snippets().forEach(snippet => {
-          snippet.aceAutoExpand = false;
-          snippet.statement_raw.valueHasMutated();
-          if (
-            snippet.result.handle().statements_count > 1 &&
-            snippet.result.handle().start != null &&
-            snippet.result.handle().end != null
-          ) {
-            const aceLineOffset = snippet.result.handle().aceLineOffset || 0;
-            snippet.result.statement_range({
-              start: {
-                row: snippet.result.handle().start.row + aceLineOffset,
-                column: snippet.result.handle().start.column
-              },
-              end: {
-                row: snippet.result.handle().end.row + aceLineOffset,
-                column: snippet.result.handle().end.column
-              }
-            });
-            snippet.result.statement_range.valueHasMutated();
-          }
+  init() {
+    const self = this;
+    if (self.editorId) {
+      self.openNotebook(self.editorId);
+    } else if (window.location.getParameter('editor') !== '') {
+      self.openNotebook(window.location.getParameter('editor'));
+    } else if (self.notebooks.length > 0) {
+      self.loadNotebook(self.notebooks[0]); // Old way of loading json for /browse
+    } else if (window.location.getParameter('type') !== '') {
+      self.newNotebook(window.location.getParameter('type'));
+    } else {
+      self.newNotebook();
+    }
+  }
 
-          snippet.previousChartOptions = self._getPreviousChartOptions(snippet);
-        });
+  loadNotebook(notebookRaw, queryTab) {
+    const self = this;
+    let currentQueries;
+    if (self.selectedNotebook() != null) {
+      currentQueries = self.selectedNotebook().unload();
+    }
 
-        if (notebook.snippets()[0].result.data().length > 0) {
-          $(document).trigger('redrawResults');
-        } else if (queryTab) {
-          notebook.snippets()[0].currentQueryTab(queryTab);
-        }
+    const notebook = new Notebook(self, notebookRaw);
 
-        if (notebook.isSaved()) {
-          notebook.snippets()[0].currentQueryTab('savedQueries');
-          if (notebook.snippets()[0].queries().length === 0) {
-            notebook.snippets()[0].fetchQueries(); // Subscribe not updating yet
-          }
+    if (notebook.snippets().length > 0) {
+      huePubSub.publish('detach.scrolls', notebook.snippets()[0]);
+      notebook.selectedSnippet(notebook.snippets()[notebook.snippets().length - 1].type());
+      if (currentQueries != null) {
+        notebook.snippets()[0].queries(currentQueries);
+      }
+      notebook.snippets().forEach(snippet => {
+        snippet.aceAutoExpand = false;
+        snippet.statement_raw.valueHasMutated();
+        if (
+          snippet.result.handle().statements_count > 1 &&
+          snippet.result.handle().start != null &&
+          snippet.result.handle().end != null
+        ) {
+          const aceLineOffset = snippet.result.handle().aceLineOffset || 0;
+          snippet.result.statement_range({
+            start: {
+              row: snippet.result.handle().start.row + aceLineOffset,
+              column: snippet.result.handle().start.column
+            },
+            end: {
+              row: snippet.result.handle().end.row + aceLineOffset,
+              column: snippet.result.handle().end.column
+            }
+          });
+          snippet.result.statement_range.valueHasMutated();
         }
+
+        snippet.previousChartOptions = self.getPreviousChartOptions(snippet);
+      });
+
+      if (notebook.snippets()[0].result.data().length > 0) {
+        $(document).trigger('redrawResults');
+      } else if (queryTab) {
+        notebook.snippets()[0].currentQueryTab(queryTab);
       }
 
-      self.selectedNotebook(notebook);
-      huePubSub.publish('check.job.browser');
-      huePubSub.publish('recalculate.name.description.width');
-    };
+      if (notebook.isSaved()) {
+        notebook.snippets()[0].currentQueryTab('savedQueries');
+        if (notebook.snippets()[0].queries().length === 0) {
+          notebook.snippets()[0].fetchQueries(); // Subscribe not updating yet
+        }
+      }
+    }
 
-    self._getPreviousChartOptions = function(snippet) {
-      return {
-        chartLimit:
-          typeof snippet.chartLimit() !== 'undefined'
-            ? snippet.chartLimit()
-            : snippet.previousChartOptions.chartLimit,
-        chartX:
-          typeof snippet.chartX() !== 'undefined'
-            ? snippet.chartX()
-            : snippet.previousChartOptions.chartX,
-        chartXPivot:
-          typeof snippet.chartXPivot() !== 'undefined'
-            ? snippet.chartXPivot()
-            : snippet.previousChartOptions.chartXPivot,
-        chartYSingle:
-          typeof snippet.chartYSingle() !== 'undefined'
-            ? snippet.chartYSingle()
-            : snippet.previousChartOptions.chartYSingle,
-        chartMapType:
-          typeof snippet.chartMapType() !== 'undefined'
-            ? snippet.chartMapType()
-            : snippet.previousChartOptions.chartMapType,
-        chartMapLabel:
-          typeof snippet.chartMapLabel() !== 'undefined'
-            ? snippet.chartMapLabel()
-            : snippet.previousChartOptions.chartMapLabel,
-        chartMapHeat:
-          typeof snippet.chartMapHeat() !== 'undefined'
-            ? snippet.chartMapHeat()
-            : snippet.previousChartOptions.chartMapHeat,
-        chartYMulti:
-          typeof snippet.chartYMulti() !== 'undefined'
-            ? snippet.chartYMulti()
-            : snippet.previousChartOptions.chartYMulti,
-        chartScope:
-          typeof snippet.chartScope() !== 'undefined'
-            ? snippet.chartScope()
-            : snippet.previousChartOptions.chartScope,
-        chartTimelineType:
-          typeof snippet.chartTimelineType() !== 'undefined'
-            ? snippet.chartTimelineType()
-            : snippet.previousChartOptions.chartTimelineType,
-        chartSorting:
-          typeof snippet.chartSorting() !== 'undefined'
-            ? snippet.chartSorting()
-            : snippet.previousChartOptions.chartSorting,
-        chartScatterGroup:
-          typeof snippet.chartScatterGroup() !== 'undefined'
-            ? snippet.chartScatterGroup()
-            : snippet.previousChartOptions.chartScatterGroup,
-        chartScatterSize:
-          typeof snippet.chartScatterSize() !== 'undefined'
-            ? snippet.chartScatterSize()
-            : snippet.previousChartOptions.chartScatterSize
-      };
-    };
+    self.selectedNotebook(notebook);
+    huePubSub.publish('check.job.browser');
+    huePubSub.publish('recalculate.name.description.width');
+  }
 
-    self.openNotebook = function(uuid, queryTab, skipUrlChange, callback) {
-      const deferredOpen = new $.Deferred();
-      $.get(
-        '/desktop/api2/doc/',
-        {
-          uuid: uuid,
-          data: true,
-          dependencies: true
-        },
-        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));
-                huePubSub.publish('active.snippet.type.changed', self.editorType());
-                self.changeURL(
-                  self.URLS.editor + '?editor=' + data.document.id + '&type=' + self.editorType()
-                );
-              } else {
-                self.changeURL(self.URLS.notebook + '?notebook=' + data.document.id);
-              }
-            }
-            if (typeof callback !== 'undefined') {
-              callback();
-            }
-            deferredOpen.resolve();
-          } else {
-            $(document).trigger('error', data.message);
-            deferredOpen.reject();
-            self.newNotebook();
+  newNotebook(editorType, callback, queryTab) {
+    const self = this;
+    huePubSub.publish('active.snippet.type.changed', editorType);
+    $.post(
+      '/notebook/api/create_notebook',
+      {
+        type: editorType || self.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());
           }
+          huePubSub.publish('active.snippet.type.changed', editorType);
         }
-      );
-      return deferredOpen.promise();
-    };
 
-    self.newNotebook = function(editorType, callback, queryTab) {
-      huePubSub.publish('active.snippet.type.changed', editorType);
-      $.post(
-        '/notebook/api/create_notebook',
-        {
-          type: editorType || options.editor_type,
-          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());
+        if (typeof callback !== 'undefined' && callback !== null) {
+          callback();
+        }
+      }
+    );
+  }
+
+  openNotebook(uuid, queryTab, skipUrlChange, callback) {
+    const self = this;
+    const deferredOpen = new $.Deferred();
+    $.get(
+      '/desktop/api2/doc/',
+      {
+        uuid: uuid,
+        data: true,
+        dependencies: true
+      },
+      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));
+              huePubSub.publish('active.snippet.type.changed', self.editorType());
+              self.changeURL(
+                self.URLS.editor + '?editor=' + data.document.id + '&type=' + self.editorType()
+              );
+            } else {
+              self.changeURL(self.URLS.notebook + '?notebook=' + data.document.id);
             }
-            huePubSub.publish('active.snippet.type.changed', editorType);
           }
-
-          if (typeof callback !== 'undefined' && callback !== null) {
+          if (typeof callback !== 'undefined') {
             callback();
           }
+          deferredOpen.resolve();
+        } else {
+          $(document).trigger('error', data.message);
+          deferredOpen.reject();
+          self.newNotebook();
         }
-      );
-    };
+      }
+    );
+    return deferredOpen.promise();
+  }
 
-    self.saveNotebook = function() {
-      self.selectedNotebook().save();
-    };
+  prepareShareModal() {
+    const self = this;
+    const selectedNotebookUuid = self.selectedNotebook() && self.selectedNotebook().uuid();
+    window.shareViewModel.setDocUuid(selectedNotebookUuid);
+    window.openShareModal();
+  }
 
-    self.saveAsNotebook = function() {
-      self.selectedNotebook().id(null);
-      self.selectedNotebook().uuid(hueUtils.UUID());
-      self.selectedNotebook().parentSavedQueryUuid(null);
-      self.selectedNotebook().save(() => {
-        huePubSub.publish('assist.document.refresh');
+  removeSnippet(notebook, snippet) {
+    const self = this;
+    let hasContent = snippet.statement_raw().length > 0;
+    if (!hasContent) {
+      snippet.properties().forEach(value => {
+        hasContent = hasContent || (ko.isObservable(value) && value().length > 0);
       });
-    };
+    }
+    if (hasContent) {
+      self.removeSnippetConfirmation({ notebook: notebook, snippet: snippet });
+      $('#removeSnippetModal' + self.suffix).modal('show');
+    } else {
+      notebook.snippets.remove(snippet);
+      window.setTimeout(() => {
+        $(document).trigger('editorSizeChanged');
+      }, 100);
+    }
+  }
 
-    self.showContextPopover = function(field, event) {
-      const $source = $(
-        event.target && event.target.nodeName !== 'A' ? event.target.parentElement : event.target
-      );
-      const offset = $source.offset();
-      huePubSub.publish('context.popover.show', {
-        data: {
-          type: 'catalogEntry',
-          catalogEntry: field.catalogEntry
-        },
-        onSampleClick: field.value,
-        showInAssistEnabled: true,
-        sourceType: self.editorType(),
-        orientation: 'bottom',
-        defaultDatabase: 'default',
-        pinEnabled: false,
-        source: {
-          element: event.target,
-          left: offset.left,
-          top: offset.top - 3,
-          right: offset.left + $source.width() + 1,
-          bottom: offset.top + $source.height() - 3
+  saveAsNotebook() {
+    const self = this;
+    self.selectedNotebook().id(null);
+    self.selectedNotebook().uuid(hueUtils.UUID());
+    self.selectedNotebook().parentSavedQueryUuid(null);
+    self.selectedNotebook().save(() => {
+      huePubSub.publish('assist.document.refresh');
+    });
+  }
+
+  saveNotebook() {
+    const self = this;
+    self.selectedNotebook().save();
+  }
+
+  showContextPopover(field, event) {
+    const self = this;
+    const $source = $(
+      event.target && event.target.nodeName !== 'A' ? event.target.parentElement : event.target
+    );
+    const offset = $source.offset();
+    huePubSub.publish('context.popover.show', {
+      data: {
+        type: 'catalogEntry',
+        catalogEntry: field.catalogEntry
+      },
+      onSampleClick: field.value,
+      showInAssistEnabled: true,
+      sourceType: self.editorType(),
+      orientation: 'bottom',
+      defaultDatabase: 'default',
+      pinEnabled: false,
+      source: {
+        element: event.target,
+        left: offset.left,
+        top: offset.top - 3,
+        right: offset.left + $source.width() + 1,
+        bottom: offset.top + $source.height() - 3
+      }
+    });
+  }
+
+  toggleEditing() {
+    const self = this;
+    self.isEditing(!self.isEditing());
+  }
+
+  toggleEditorMode() {
+    const self = this;
+    const _notebook = self.selectedNotebook();
+    const _newSnippets = [];
+
+    if (self.editorType() !== 'notebook') {
+      self.editorType('notebook');
+      self.preEditorTogglingSnippet(_notebook.snippets()[0]);
+      const _variables = _notebook.snippets()[0].variables();
+      const _statementKeys = [];
+      // Split statements
+      _notebook.type('notebook');
+      _notebook
+        .snippets()[0]
+        .statementsList()
+        .forEach(sql_statement => {
+          let _snippet;
+          if (sql_statement.hashCode() in _notebook.presentationSnippets()) {
+            _snippet = _notebook.presentationSnippets()[sql_statement.hashCode()]; // Persist result
+            _snippet.variables(_variables);
+          } else {
+            const _title = [];
+            const _statement = [];
+            sql_statement
+              .trim()
+              .split('\n')
+              .forEach(line => {
+                if (line.trim().startsWith('--') && _statement.length === 0) {
+                  _title.push(line.substr(2));
+                } else {
+                  _statement.push(line);
+                }
+              });
+            _snippet = new Snippet(self, _notebook, {
+              type: _notebook.initialType,
+              statement_raw: _statement.join('\n'),
+              result: {},
+              name: _title.join('\n'),
+              variables: komapping.toJS(_variables)
+            });
+            _snippet.variables = _notebook.snippets()[0].variables;
+            _snippet.init();
+            _notebook.presentationSnippets()[sql_statement.hashCode()] = _snippet;
+          }
+          _statementKeys.push(sql_statement.hashCode());
+          _newSnippets.push(_snippet);
+        });
+      $.each(_notebook.presentationSnippets(), key => {
+        // Dead statements
+        if (!key in _statementKeys) {
+          delete _notebook.presentationSnippets()[key];
         }
       });
-    };
+    } else {
+      self.editorType(_notebook.initialType);
+      // Revert to one statement
+      _newSnippets.push(self.preEditorTogglingSnippet());
+      _notebook.type('query-' + _notebook.initialType);
+    }
+    _notebook.snippets(_newSnippets);
+    _newSnippets.forEach(snippet => {
+      huePubSub.publish('editor.redraw.data', { snippet: snippet });
+    });
   }
 
-  prepareShareModal() {
-    const selectedNotebookUuid = this.selectedNotebook() && this.selectedNotebook().uuid();
-    window.shareViewModel.setDocUuid(selectedNotebookUuid);
-    window.openShareModal();
+  togglePresentationMode() {
+    const self = this;
+    if (self.selectedNotebook().initialType !== 'notebook') {
+      self.toggleEditorMode();
+    }
   }
 }
 

+ 683 - 733
desktop/core/src/desktop/js/apps/notebook2/notebook.js

@@ -24,7 +24,7 @@ import huePubSub from 'utils/huePubSub';
 import hueUtils from 'utils/hueUtils';
 
 import Session from 'apps/notebook2/session';
-import Snippet from 'apps/notebook2/snippet';
+import { Snippet, STATUS as SNIPPET_STATUS } from 'apps/notebook2/snippet';
 
 const NOTEBOOK_MAPPING = {
   ignore: [
@@ -63,68 +63,23 @@ class Notebook {
   constructor(vm, notebook) {
     const self = this;
 
-    self.id = ko.observable(
-      typeof notebook.id != 'undefined' && notebook.id != null ? notebook.id : null
-    );
-    self.uuid = ko.observable(
-      typeof notebook.uuid != 'undefined' && notebook.uuid != null ? notebook.uuid : hueUtils.UUID()
-    );
-    self.name = ko.observable(
-      typeof notebook.name != 'undefined' && notebook.name != null ? notebook.name : 'My Notebook'
-    );
-    self.description = ko.observable(
-      typeof notebook.description != 'undefined' && notebook.description != null
-        ? notebook.description
-        : ''
-    );
-    self.type = ko.observable(
-      typeof notebook.type != 'undefined' && notebook.type != null ? notebook.type : 'notebook'
-    );
+    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(
-      typeof notebook.coordinatorUuid != 'undefined' && notebook.coordinatorUuid != null
-        ? notebook.coordinatorUuid
-        : null
-    );
-    self.isHistory = ko.observable(
-      typeof notebook.is_history != 'undefined' && notebook.is_history != null
-        ? notebook.is_history
-        : false
-    );
-    self.isManaged = ko.observable(
-      typeof notebook.isManaged != 'undefined' && notebook.isManaged != null
-        ? notebook.isManaged
-        : false
-    );
-    self.parentSavedQueryUuid = ko.observable(
-      typeof notebook.parentSavedQueryUuid != 'undefined' && notebook.parentSavedQueryUuid != null
-        ? notebook.parentSavedQueryUuid
-        : null
-    ); // History parent
-    self.isSaved = ko.observable(
-      typeof notebook.isSaved != 'undefined' && notebook.isSaved != null ? notebook.isSaved : false
-    );
-    self.canWrite = ko.observable(
-      typeof notebook.can_write != 'undefined' && notebook.can_write != null
-        ? notebook.can_write
-        : true
-    );
-    self.onSuccessUrl = ko.observable(
-      typeof notebook.onSuccessUrl != 'undefined' && notebook.onSuccessUrl != null
-        ? notebook.onSuccessUrl
-        : null
-    );
-    self.pubSubUrl = ko.observable(
-      typeof notebook.pubSubUrl != 'undefined' && notebook.pubSubUrl != null
-        ? notebook.pubSubUrl
-        : null
-    );
-    self.isPresentationModeDefault = ko.observable(
-      typeof notebook.isPresentationModeDefault != 'undefined' &&
-        notebook.isPresentationModeDefault != null
-        ? notebook.isPresentationModeDefault
-        : false
-    );
+    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 => {
@@ -138,79 +93,62 @@ class Notebook {
       }
     });
     self.presentationSnippets = ko.observable({});
-    self.isHidingCode = ko.observable(
-      typeof notebook.isHidingCode != 'undefined' && notebook.isHidingCode != null
-        ? notebook.isHidingCode
-        : false
-    );
+    self.isHidingCode = ko.observable(!!notebook.isHidingCode);
 
     self.snippets = ko.observableArray();
     self.selectedSnippet = ko.observable(vm.editorType()); // Aka selectedSnippetType
     self.creatingSessionLocks = ko.observableArray();
-    self.sessions = komapping.fromJS(
-      typeof notebook.sessions != 'undefined' && notebook.sessions != null ? notebook.sessions : [],
-      {
-        create: function(value) {
-          return new Session(vm, value.data);
-        }
-      }
-    );
-    self.directoryUuid = ko.observable(
-      typeof notebook.directoryUuid != 'undefined' && notebook.directoryUuid != null
-        ? notebook.directoryUuid
-        : null
-    );
-    self.dependents = komapping.fromJS(
-      typeof notebook.dependents != 'undefined' && notebook.dependents != null
-        ? notebook.dependents
-        : []
-    );
-    self.dependentsCoordinator = ko.computed(() => {
-      return $.grep(self.dependents(), doc => {
-        return doc.type() == 'oozie-coordinator2' && doc.is_managed() == true;
-      });
+    self.sessions = komapping.fromJS(notebook.sessions || [], {
+      create: value => new Session(vm, value.data)
     });
+    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())
+    );
     if (self.dependentsCoordinator().length > 0 && !self.coordinatorUuid()) {
       self.coordinatorUuid(self.dependentsCoordinator()[0].uuid());
     }
     self.history = ko.observableArray(
       vm.selectedNotebook() &&
         vm.selectedNotebook().history().length > 0 &&
-        vm.selectedNotebook().history()[0].type == self.type()
+        vm.selectedNotebook().history()[0].type === self.type()
         ? vm.selectedNotebook().history()
         : []
     );
     self.history.subscribe(val => {
       if (
         self.id() == null &&
-        val.length == 0 &&
+        val.length === 0 &&
         self.historyFilter() === '' &&
         !vm.isNotificationManager()
       ) {
         self
           .snippets()[0]
           .currentQueryTab(
-            typeof IS_EMBEDDED !== 'undefined' && IS_EMBEDDED ? 'queryHistory' : 'savedQueries'
+            typeof window.IS_EMBEDDED !== 'undefined' && window.IS_EMBEDDED
+              ? 'queryHistory'
+              : 'savedQueries'
           );
       }
     });
     self.historyFilter = ko.observable('');
     self.historyFilterVisible = ko.observable(false);
     self.historyFilter.extend({ rateLimit: { method: 'notifyWhenChangesStop', timeout: 900 } });
-    self.historyFilter.subscribe(val => {
-      if (self.historyCurrentPage() != 1) {
+    self.historyFilter.subscribe(() => {
+      if (self.historyCurrentPage() !== 1) {
         self.historyCurrentPage(1);
       } else {
         self.fetchHistory();
       }
     });
-    self.loadingHistory = ko.observable(self.history().length == 0);
+    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(
       vm.selectedNotebook() ? vm.selectedNotebook().historyCurrentPage() : 1
     );
-    self.historyCurrentPage.subscribe(val => {
+    self.historyCurrentPage.subscribe(() => {
       self.fetchHistory();
     });
     self.historyTotalPages = ko.observable(
@@ -220,22 +158,12 @@ class Notebook {
     self.schedulerViewModel = null;
     self.schedulerViewModelIsLoaded = ko.observable(false);
     self.schedulerViewerViewModel = ko.observable();
-    self.isBatchable = ko.computed(() => {
-      return (
-        self.snippets().length > 0 &&
-        $.grep(self.snippets(), snippet => {
-          return snippet.isBatchable();
-        }).length == self.snippets().length
-      );
-    });
+    self.isBatchable = ko.pureComputed(
+      () => self.snippets().length > 0 && self.snippets().every(snippet => snippet.isBatchable())
+    );
 
     self.isExecutingAll = ko.observable(!!notebook.isExecutingAll);
-    self.cancelExecutingAll = function() {
-      const index = self.executingAllIndex();
-      if (self.isExecutingAll() && self.snippets()[index]) {
-        self.snippets()[index].cancel();
-      }
-    };
+
     self.executingAllIndex = ko.observable(notebook.executingAllIndex || 0);
 
     self.retryModalConfirm = null;
@@ -245,629 +173,11 @@ class Notebook {
 
     self.canSave = vm.canSave;
 
-    self.getSession = function(session_type) {
-      let _s = null;
-      $.each(self.sessions(), (index, s) => {
-        if (s.type() == session_type) {
-          _s = s;
-          return false;
-        }
-      });
-      return _s;
-    };
-
-    self.getSnippets = function(type) {
-      return $.grep(self.snippets(), snippet => {
-        return snippet.type() == type;
-      });
-    };
-
     self.unloaded = ko.observable(false);
-    self.unload = function() {
-      self.unloaded(true);
-      let currentQueries = null;
-      self.snippets().forEach(snippet => {
-        if (snippet.checkStatusTimeout != null) {
-          clearTimeout(snippet.checkStatusTimeout);
-          snippet.checkStatusTimeout = null;
-        }
-        if (currentQueries == null) {
-          currentQueries = snippet.queries();
-        }
-      });
-      return currentQueries;
-    };
-
-    self.restartSession = function(session, callback) {
-      if (session.restarting()) {
-        return;
-      }
-      session.restarting(true);
-      const snippets = self.getSnippets(session.type());
-
-      $.each(snippets, (index, snippet) => {
-        snippet.status('loading');
-      });
-
-      self.closeSession(session, true, () => {
-        self.createSession(
-          session,
-          () => {
-            $.each(snippets, (index, snippet) => {
-              snippet.status('ready');
-            });
-            session.restarting(false);
-            if (callback) {
-              callback();
-            }
-          },
-          () => {
-            session.restarting(false);
-          }
-        );
-      });
-    };
-
-    self.addSession = function(session) {
-      const toRemove = [];
-      $.each(self.sessions(), (index, s) => {
-        if (s.type() == session.type()) {
-          toRemove.push(s);
-        }
-      });
-
-      $.each(toRemove, (index, s) => {
-        self.sessions.remove(s);
-      });
-
-      self.sessions.push(session);
-    };
-
-    self.addSnippet = function(snippet, skipSession) {
-      const _snippet = new Snippet(vm, self, snippet);
-      self.snippets.push(_snippet);
-
-      if (self.getSession(_snippet.type()) == null && typeof skipSession == 'undefined') {
-        window.setTimeout(() => {
-          _snippet.status('loading');
-          self.createSession(new Session(vm, { type: _snippet.type() }));
-        }, 200);
-      }
-
-      _snippet.init();
-      return _snippet;
-    };
-
-    self.createSession = function(session, callback, failCallback) {
-      if (self.creatingSessionLocks().indexOf(session.type()) != -1) {
-        // Create one type of session max
-        return;
-      } else {
-        self.creatingSessionLocks.push(session.type());
-      }
-
-      let compute = null;
-      $.each(self.getSnippets(session.type()), (index, snippet) => {
-        snippet.status('loading');
-        if (index == 0) {
-          compute = snippet.compute();
-        }
-      });
-
-      const fail = function(message) {
-        $.each(self.getSnippets(session.type()), (index, snippet) => {
-          snippet.status('failed');
-        });
-        $(document).trigger('error', message);
-        if (failCallback) {
-          failCallback();
-        }
-      };
-
-      $.post(
-        '/notebook/api/create_session',
-        {
-          notebook: komapping.toJSON(self.getContext()),
-          session: komapping.toJSON(session), // e.g. {'type': 'pyspark', 'properties': [{'name': driverCores', 'value', '2'}]}
-          cluster: komapping.toJSON(compute ? compute : '')
-        },
-        data => {
-          if (data.status == 0) {
-            komapping.fromJS(data.session, {}, session);
-            if (self.getSession(session.type()) == null) {
-              self.addSession(session);
-            }
-            $.each(self.getSnippets(session.type()), (index, snippet) => {
-              snippet.status('ready');
-            });
-            if (callback) {
-              setTimeout(callback, 500);
-            }
-          } else if (data.status == 401) {
-            $(document).trigger('showAuthModal', { type: session.type() });
-          } else {
-            fail(data.message);
-          }
-        }
-      )
-        .fail(xhr => {
-          if (xhr.status !== 502) {
-            fail(xhr.responseText);
-          }
-        })
-        .always(() => {
-          self.creatingSessionLocks.remove(session.type());
-        });
-    };
-
-    self.authSession = function() {
-      self.createSession(
-        new Session(vm, {
-          type: vm.authSessionType(),
-          properties: [
-            { name: 'user', value: vm.authSessionUsername() },
-            { name: 'password', value: vm.authSessionPassword() }
-          ]
-        }),
-        vm.authSessionCallback() // On new session we don't automatically execute the snippet after the aut. On session expiration we do or we refresh assist DB when login-in.
-      );
-    };
-
-    self.newSnippet = function(type) {
-      if (type) {
-        self.selectedSnippet(type);
-      }
-      const snippet = self.addSnippet({
-        type: self.selectedSnippet(),
-        result: {}
-      });
-
-      window.setTimeout(() => {
-        const lastSnippet = snippet;
-        if (lastSnippet.ace() != null) {
-          lastSnippet.ace().focus();
-        }
-      }, 100);
-
-      hueAnalytics.log('notebook', 'add_snippet/' + (type ? type : self.selectedSnippet()));
-      return snippet;
-    };
-
-    self.newSnippetAbove = function(id) {
-      self.newSnippet();
-      let idx = 0;
-      self.snippets().forEach((snippet, cnt) => {
-        if (snippet.id() == id) {
-          idx = cnt;
-        }
-      });
-      self.snippets(self.snippets().move(self.snippets().length - 1, idx));
-    };
-
-    self.getContext = function() {
-      return {
-        id: self.id,
-        uuid: self.uuid,
-        parentSavedQueryUuid: self.parentSavedQueryUuid,
-        isSaved: self.isSaved,
-        sessions: self.sessions,
-        type: self.type,
-        name: self.name
-      };
-    };
-
-    self.save = function(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);
-      $.each(
-        cp.snippets.concat(
-          Object.keys(cp.presentationSnippets).map(key => {
-            return cp.presentationSnippets[key];
-          })
-        ),
-        (index, 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 =
-        vm.editorMode() || (self.isPresentationMode() && vm.editorType() != 'notebook'); // Editor should not convert to Notebook in presentation mode
-
-      $.post(
-        '/notebook/api/notebook/save',
-        {
-          notebook: komapping.toJSON(cp, NOTEBOOK_MAPPING),
-          editorMode: editorMode
-        },
-        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 => {
-                    return 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 (
-                self.snippets()[0].downloadResultViewModel &&
-                self
-                  .snippets()[0]
-                  .downloadResultViewModel()
-                  .saveTarget() === 'dashboard'
-              ) {
-                huePubSub.publish(
-                  'open.link',
-                  vm.URLS.report +
-                    '&uuid=' +
-                    data.uuid +
-                    '&statement=' +
-                    self.snippets()[0].result.handle().statement_id
-                );
-              } else {
-                vm.changeURL(vm.URLS.editor + '?editor=' + data.id);
-              }
-            } else {
-              vm.changeURL(vm.URLS.notebook + '?notebook=' + data.id);
-            }
-            if (typeof callback == 'function') {
-              callback();
-            }
-          } else {
-            $(document).trigger('error', data.message);
-          }
-        }
-      ).fail((xhr, textStatus, errorThrown) => {
-        if (xhr.status !== 502) {
-          $(document).trigger('error', xhr.responseText);
-        }
-      });
-    };
-
-    self.close = function() {
-      hueAnalytics.log('notebook', 'close');
-      $.post('/notebook/api/notebook/close', {
-        notebook: komapping.toJSON(self, NOTEBOOK_MAPPING),
-        editorMode: vm.editorMode()
-      });
-    };
-
-    self.clearResults = function() {
-      $.each(self.snippets(), (index, snippet) => {
-        snippet.result.clear();
-        snippet.status('ready');
-      });
-    };
-
-    self.executeAll = function() {
-      if (self.isExecutingAll() || self.snippets().length === 0) {
-        return;
-      }
-
-      self.isExecutingAll(true);
-      self.executingAllIndex(0);
-
-      self.snippets()[self.executingAllIndex()].execute();
-    };
-
-    self.saveDefaultUserProperties = function(session) {
-      apiHelper.saveConfiguration({
-        app: session.type(),
-        properties: session.properties,
-        userId: vm.userId
-      });
-    };
-
-    self.closeAndRemoveSession = function(session) {
-      self.closeSession(session, false, () => {
-        self.sessions.remove(session);
-      });
-    };
-
-    self.closeSession = function(session, silent, callback) {
-      $.post(
-        '/notebook/api/close_session',
-        {
-          session: komapping.toJSON(session)
-        },
-        data => {
-          if (!silent && data && data.status != 0 && data.status != -2 && data.message) {
-            $(document).trigger('error', data.message);
-          }
-
-          if (callback) {
-            callback();
-          }
-        }
-      ).fail(xhr => {
-        if (!silent && xhr.status !== 502) {
-          $(document).trigger('error', xhr.responseText);
-        }
-      });
-    };
-
-    self.fetchHistory = function(callback) {
-      const QUERIES_PER_PAGE = 50;
-      self.loadingHistory(true);
-
-      $.get(
-        '/notebook/api/get_history',
-        {
-          doc_type: self.selectedSnippet(),
-          limit: QUERIES_PER_PAGE,
-          page: self.historyCurrentPage(),
-          doc_text: self.historyFilter(),
-          is_notification_manager: vm.isNotificationManager()
-        },
-        data => {
-          const parsedHistory = [];
-          if (data && data.history) {
-            data.history.forEach(nbk => {
-              parsedHistory.push(
-                self._makeHistoryRecord(
-                  nbk.absoluteUrl,
-                  nbk.data.statement,
-                  nbk.data.lastExecuted,
-                  nbk.data.status,
-                  nbk.name,
-                  nbk.uuid
-                )
-              );
-            });
-          }
-          self.history(parsedHistory);
-          self.historyTotalPages(Math.ceil(data.count / QUERIES_PER_PAGE));
-        }
-      ).always(() => {
-        self.loadingHistory(false);
-        if (callback) {
-          callback();
-        }
-      });
-    };
-
-    self.prevHistoryPage = function() {
-      if (self.historyCurrentPage() !== 1) {
-        self.historyCurrentPage(self.historyCurrentPage() - 1);
-      }
-    };
-
-    self.nextHistoryPage = function() {
-      if (self.historyCurrentPage() < self.historyTotalPages()) {
-        self.historyCurrentPage(self.historyCurrentPage() + 1);
-      }
-    };
-
     self.updateHistoryFailed = false;
-    self.updateHistory = function(statuses, interval) {
-      let items = $.grep(self.history(), item => {
-        return statuses.indexOf(item.status()) != -1;
-      }).slice(0, 25);
-
-      function updateHistoryCall(item) {
-        $.post('/notebook/api/check_status', {
-          notebook: komapping.toJSON({ id: item.uuid() })
-        })
-          .done(data => {
-            const status =
-              data.status == -3
-                ? 'expired'
-                : data.status == 0
-                ? data.query_status.status
-                : 'failed';
-            if (status && item.status() != status) {
-              item.status(status);
-            }
-          })
-          .fail(xhr => {
-            items = [];
-            self.updateHistoryFailed = true;
-            console.warn('Lost connectivity to the Hue history refresh backend.');
-          })
-          .always(() => {
-            if (items.length > 0) {
-              window.setTimeout(() => {
-                updateHistoryCall(items.pop());
-              }, 1000);
-            } else if (!self.updateHistoryFailed) {
-              window.setTimeout(() => {
-                self.updateHistory(statuses, interval);
-              }, interval);
-            }
-          });
-      }
-
-      if (items.length > 0) {
-        updateHistoryCall(items.pop());
-      } else if (!self.updateHistoryFailed) {
-        window.setTimeout(() => {
-          self.updateHistory(statuses, interval);
-        }, interval);
-      }
-    };
-
-    self._makeHistoryRecord = function(url, statement, lastExecuted, status, name, uuid) {
-      return komapping.fromJS({
-        url: url,
-        query: statement.substring(0, 1000) + (statement.length > 1000 ? '...' : ''),
-        lastExecuted: lastExecuted,
-        status: status,
-        name: name,
-        uuid: uuid
-      });
-    };
-
-    self.clearHistory = function(type) {
-      hueAnalytics.log('notebook', 'clearHistory');
-      $.post(
-        '/notebook/api/clear_history',
-        {
-          notebook: komapping.toJSON(self.getContext()),
-          doc_type: self.selectedSnippet(),
-          is_notification_manager: vm.isNotificationManager()
-        },
-        data => {
-          self.history.removeAll();
-          if (self.isHistory()) {
-            self.id(null);
-            self.uuid(hueUtils.UUID());
-            vm.changeURL(vm.URLS.editor + '?type=' + vm.editorType());
-          }
-        }
-      ).fail(xhr => {
-        if (xhr.status !== 502) {
-          $(document).trigger('error', xhr.responseText);
-        }
-      });
-      $(document).trigger('hideHistoryModal');
-    };
-
-    self.loadScheduler = function() {
-      if (typeof vm.CoordinatorEditorViewModel !== 'undefined' && self.isBatchable()) {
-        let _action;
-        if (self.coordinatorUuid()) {
-          _action = 'edit';
-        } else {
-          _action = 'new';
-        }
-        hueAnalytics.log('notebook', 'schedule/' + _action);
-
-        const getCoordinator = function() {
-          $.get(
-            '/oozie/editor/coordinator/' + _action + '/',
-            {
-              format: 'json',
-              document: self.uuid(),
-              coordinator: self.coordinatorUuid()
-            },
-            data => {
-              if ($('#schedulerEditor').length > 0) {
-                huePubSub.publish('hue4.process.headers', {
-                  response: data.layout,
-                  callback: function(r) {
-                    $('#schedulerEditor').html(r);
-
-                    self.schedulerViewModel = new vm.CoordinatorEditorViewModel(
-                      data.coordinator,
-                      data.credentials,
-                      data.workflows,
-                      data.can_edit
-                    );
-
-                    ko.cleanNode($('#schedulerEditor')[0]);
-                    ko.applyBindings(self.schedulerViewModel, $('#schedulerEditor')[0]);
-                    $(document).off('showSubmitPopup');
-                    $(document).on('showSubmitPopup', (event, data) => {
-                      $('.submit-modal-editor').html(data);
-                      $('.submit-modal-editor').modal('show');
-                      $('.submit-modal-editor').on('hidden', () => {
-                        huePubSub.publish('hide.datepicker');
-                      });
-                      const _sel = $('.submit-form .control-group[rel!="popover"]:visible');
-                      if (_sel.length > 0) {
-                        $('.submit-modal-editor .modal-body').height(
-                          $('.submit-modal-editor .modal-body').height() + 60
-                        );
-                      }
-                    });
-
-                    huePubSub.publish('render.jqcron');
-
-                    self.schedulerViewModel.coordinator.properties.cron_advanced.valueHasMutated(); // Update jsCron enabled status
-                    self.schedulerViewModel.coordinator.tracker().markCurrentStateAsClean();
-                    self.schedulerViewModel.isEditing(true);
-
-                    self.schedulerViewModelIsLoaded(true);
 
-                    if (_action == 'new') {
-                      self.schedulerViewModel.coordinator.properties.document(self.uuid()); // Expected for triggering the display
-                    }
-                  }
-                });
-              }
-            }
-          ).fail(xhr => {
-            if (xhr.status !== 502) {
-              $(document).trigger('error', xhr.responseText);
-            }
-          });
-        };
-
-        getCoordinator();
-      }
-    };
-
-    self.saveScheduler = function() {
-      if (
-        self.isBatchable() &&
-        (!self.coordinatorUuid() || self.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();
-          }
-        });
-      }
-    };
-
-    self.showSubmitPopup = function() {
-      $.get(
-        '/oozie/editor/coordinator/submit/' + self.coordinatorUuid(),
-        {
-          format: 'json'
-        },
-        data => {
-          $(document).trigger('showSubmitPopup', data);
-        }
-      ).fail((xhr, textStatus, errorThrown) => {
-        if (xhr.status !== 502) {
-          $(document).trigger('error', xhr.responseText);
-        }
-      });
-    };
-
-    self.viewSchedulerId = ko.observable(
-      typeof notebook.viewSchedulerId != 'undefined' && notebook.viewSchedulerId != null
-        ? notebook.viewSchedulerId
-        : ''
-    );
-    self.viewSchedulerId.subscribe(newVal => {
+    self.viewSchedulerId = ko.observable(notebook.viewSchedulerId || '');
+    self.viewSchedulerId.subscribe(() => {
       self.save();
     });
     self.isSchedulerJobRunning = ko.observable();
@@ -875,7 +185,7 @@ class Notebook {
 
     // Init
     if (notebook.snippets) {
-      $.each(notebook.snippets, (index, snippet) => {
+      notebook.snippets.forEach(snippet => {
         self.addSnippet(snippet);
       });
       if (
@@ -887,11 +197,11 @@ class Notebook {
           snippet.status = 'ready'; // Protect from storm of check_statuses
           const _snippet = new Snippet(vm, self, snippet);
           _snippet.init();
-          _snippet.previousChartOptions = vm._getPreviousChartOptions(_snippet);
+          _snippet.previousChartOptions = vm.getPreviousChartOptions(_snippet);
           self.presentationSnippets()[key] = _snippet;
         });
       }
-      if (vm.editorMode() && self.history().length == 0) {
+      if (vm.editorMode() && self.history().length === 0) {
         self.fetchHistory(() => {
           self.updateHistory(['starting', 'running'], 30000);
           self.updateHistory(['available'], 60000 * 5);
@@ -950,6 +260,646 @@ class Notebook {
 
     huePubSub.publish('assist.is.db.panel.ready');
   }
+
+  addSession(session) {
+    const self = this;
+    const toRemove = self.sessions().filter(s => s.type() === session.type());
+
+    toRemove.forEach(s => {
+      self.sessions.remove(s);
+    });
+
+    self.sessions.push(session);
+  }
+
+  addSnippet(snippet, skipSession) {
+    const self = this;
+    const newSnippet = new Snippet(self.parentVm, self, snippet);
+    self.snippets.push(newSnippet);
+
+    if (self.getSession(newSnippet.type()) == null && typeof skipSession == 'undefined') {
+      window.setTimeout(() => {
+        newSnippet.status(SNIPPET_STATUS.loading);
+        self.createSession(new Session(self.parentVm, { type: newSnippet.type() }));
+      }, 200);
+    }
+
+    newSnippet.init();
+    return newSnippet;
+  }
+
+  authSession() {
+    const self = this;
+    self.createSession(
+      new Session(self.parentVm, {
+        type: self.parentVm.authSessionType(),
+        properties: [
+          { name: 'user', value: self.parentVm.authSessionUsername() },
+          { name: 'password', value: self.parentVm.authSessionPassword() }
+        ]
+      }),
+      self.parentVm.authSessionCallback() // On new session we don't automatically execute the snippet after the aut. On session expiration we do or we refresh assist DB when login-in.
+    );
+  }
+
+  cancelExecutingAll() {
+    const self = this;
+    const index = self.executingAllIndex();
+    if (self.isExecutingAll() && self.snippets()[index]) {
+      self.snippets()[index].cancel();
+    }
+  }
+
+  clearHistory() {
+    const self = this;
+    hueAnalytics.log('notebook', 'clearHistory');
+    $.post(
+      '/notebook/api/clear_history',
+      {
+        notebook: komapping.toJSON(self.getContext(), NOTEBOOK_MAPPING),
+        doc_type: self.selectedSnippet(),
+        is_notification_manager: self.parentVm.isNotificationManager()
+      },
+      () => {
+        self.history.removeAll();
+        if (self.isHistory()) {
+          self.id(null);
+          self.uuid(hueUtils.UUID());
+          self.parentVm.changeURL(
+            self.parentVm.URLS.editor + '?type=' + self.parentVm.editorType()
+          );
+        }
+      }
+    ).fail(xhr => {
+      if (xhr.status !== 502) {
+        $(document).trigger('error', xhr.responseText);
+      }
+    });
+    $(document).trigger('hideHistoryModal');
+  }
+
+  clearResults() {
+    const self = this;
+    self.snippets().forEach(snippet => {
+      snippet.result.clear();
+      snippet.status(SNIPPET_STATUS.ready);
+    });
+  }
+
+  close() {
+    const self = this;
+    hueAnalytics.log('notebook', 'close');
+    $.post('/notebook/api/notebook/close', {
+      notebook: komapping.toJSON(self, NOTEBOOK_MAPPING),
+      editorMode: self.parentVm.editorMode()
+    });
+  }
+
+  closeAndRemoveSession(session) {
+    const self = this;
+    self.closeSession(session, false, () => {
+      self.sessions.remove(session);
+    });
+  }
+
+  closeSession(session, silent, callback) {
+    $.post(
+      '/notebook/api/close_session',
+      {
+        session: komapping.toJSON(session)
+      },
+      data => {
+        if (!silent && data && data.status !== 0 && data.status !== -2 && data.message) {
+          $(document).trigger('error', data.message);
+        }
+
+        if (callback) {
+          callback();
+        }
+      }
+    ).fail(xhr => {
+      if (!silent && xhr.status !== 502) {
+        $(document).trigger('error', xhr.responseText);
+      }
+    });
+  }
+
+  createSession(session, callback, failCallback) {
+    const self = this;
+    if (self.creatingSessionLocks().indexOf(session.type()) !== -1) {
+      // Create one type of session max
+      return;
+    } else {
+      self.creatingSessionLocks.push(session.type());
+    }
+
+    let compute = null;
+    self.getSnippets(session.type()).forEach((snippet, index) => {
+      snippet.status(SNIPPET_STATUS.loading);
+      if (index === 0) {
+        compute = snippet.compute();
+      }
+    });
+
+    const fail = function(message) {
+      self.getSnippets(session.type()).forEach(snippet => {
+        snippet.status('failed');
+      });
+      $(document).trigger('error', message);
+      if (failCallback) {
+        failCallback();
+      }
+    };
+
+    $.post(
+      '/notebook/api/create_session',
+      {
+        notebook: komapping.toJSON(self.getContext(), NOTEBOOK_MAPPING),
+        session: komapping.toJSON(session), // e.g. {'type': 'pyspark', 'properties': [{'name': driverCores', 'value', '2'}]}
+        cluster: komapping.toJSON(compute ? compute : '')
+      },
+      data => {
+        if (data.status === 0) {
+          komapping.fromJS(data.session, {}, session);
+          if (self.getSession(session.type()) == null) {
+            self.addSession(session);
+          }
+          self.getSnippets(session.type()).forEach(snippet => {
+            snippet.status('ready');
+          });
+          if (callback) {
+            setTimeout(callback, 500);
+          }
+        } else if (data.status === 401) {
+          $(document).trigger('showAuthModal', { type: session.type() });
+        } else {
+          fail(data.message);
+        }
+      }
+    )
+      .fail(xhr => {
+        if (xhr.status !== 502) {
+          fail(xhr.responseText);
+        }
+      })
+      .always(() => {
+        self.creatingSessionLocks.remove(session.type());
+      });
+  }
+
+  executeAll() {
+    const self = this;
+    if (self.isExecutingAll() || self.snippets().length === 0) {
+      return;
+    }
+
+    self.isExecutingAll(true);
+    self.executingAllIndex(0);
+
+    self.snippets()[self.executingAllIndex()].execute();
+  }
+
+  fetchHistory(callback) {
+    const self = this;
+    const QUERIES_PER_PAGE = 50;
+    self.loadingHistory(true);
+
+    $.get(
+      '/notebook/api/get_history',
+      {
+        doc_type: self.selectedSnippet(),
+        limit: QUERIES_PER_PAGE,
+        page: self.historyCurrentPage(),
+        doc_text: self.historyFilter(),
+        is_notification_manager: self.parentVm.isNotificationManager()
+      },
+      data => {
+        const parsedHistory = [];
+        if (data && data.history) {
+          data.history.forEach(nbk => {
+            parsedHistory.push(
+              self.makeHistoryRecord(
+                nbk.absoluteUrl,
+                nbk.data.statement,
+                nbk.data.lastExecuted,
+                nbk.data.status,
+                nbk.name,
+                nbk.uuid
+              )
+            );
+          });
+        }
+        self.history(parsedHistory);
+        self.historyTotalPages(Math.ceil(data.count / QUERIES_PER_PAGE));
+      }
+    ).always(() => {
+      self.loadingHistory(false);
+      if (callback) {
+        callback();
+      }
+    });
+  }
+
+  getContext() {
+    const self = this;
+    return {
+      id: self.id,
+      uuid: self.uuid,
+      parentSavedQueryUuid: self.parentSavedQueryUuid,
+      isSaved: self.isSaved,
+      sessions: self.sessions,
+      type: self.type,
+      name: self.name
+    };
+  }
+
+  getSession(session_type) {
+    const self = this;
+    let found = undefined;
+    self.sessions().every(session => {
+      if (session.type() === session_type) {
+        found = session;
+        return false;
+      }
+      return true;
+    });
+    return found;
+  }
+
+  getSnippets(type) {
+    const self = this;
+    return self.snippets().filter(snippet => snippet.type() === type);
+  }
+
+  loadScheduler() {
+    const self = this;
+    if (typeof self.parentVm.CoordinatorEditorViewModel !== 'undefined' && self.isBatchable()) {
+      let action;
+      if (self.coordinatorUuid()) {
+        action = 'edit';
+      } else {
+        action = 'new';
+      }
+      hueAnalytics.log('notebook', 'schedule/' + action);
+
+      const getCoordinator = function() {
+        $.get(
+          '/oozie/editor/coordinator/' + action + '/',
+          {
+            format: 'json',
+            document: self.uuid(),
+            coordinator: self.coordinatorUuid()
+          },
+          data => {
+            if ($('#schedulerEditor').length > 0) {
+              huePubSub.publish('hue4.process.headers', {
+                response: data.layout,
+                callback: function(r) {
+                  const $schedulerEditor = $('#schedulerEditor');
+                  $schedulerEditor.html(r);
+
+                  self.schedulerViewModel = new self.parentVm.CoordinatorEditorViewModel(
+                    data.coordinator,
+                    data.credentials,
+                    data.workflows,
+                    data.can_edit
+                  );
+
+                  ko.cleanNode($schedulerEditor[0]);
+                  ko.applyBindings(self.schedulerViewModel, $schedulerEditor[0]);
+                  $(document).off('showSubmitPopup');
+                  $(document).on('showSubmitPopup', (event, data) => {
+                    const $submitModalEditor = $('.submit-modal-editor');
+                    $submitModalEditor.html(data);
+                    $submitModalEditor.modal('show');
+                    $submitModalEditor.on('hidden', () => {
+                      huePubSub.publish('hide.datepicker');
+                    });
+                    const _sel = $('.submit-form .control-group[rel!="popover"]:visible');
+                    if (_sel.length > 0) {
+                      const $submitModalEditorBody = $('.submit-modal-editor .modal-body');
+                      $submitModalEditorBody.height($submitModalEditorBody.height() + 60);
+                    }
+                  });
+
+                  huePubSub.publish('render.jqcron');
+
+                  self.schedulerViewModel.coordinator.properties.cron_advanced.valueHasMutated(); // Update jsCron enabled status
+                  self.schedulerViewModel.coordinator.tracker().markCurrentStateAsClean();
+                  self.schedulerViewModel.isEditing(true);
+
+                  self.schedulerViewModelIsLoaded(true);
+
+                  if (action === 'new') {
+                    self.schedulerViewModel.coordinator.properties.document(self.uuid()); // Expected for triggering the display
+                  }
+                }
+              });
+            }
+          }
+        ).fail(xhr => {
+          if (xhr.status !== 502) {
+            $(document).trigger('error', xhr.responseText);
+          }
+        });
+      };
+
+      getCoordinator();
+    }
+  }
+
+  makeHistoryRecord(url, statement, lastExecuted, status, name, uuid) {
+    return komapping.fromJS({
+      url: url,
+      query: statement.substring(0, 1000) + (statement.length > 1000 ? '...' : ''),
+      lastExecuted: lastExecuted,
+      status: status,
+      name: name,
+      uuid: uuid
+    });
+  }
+
+  newSnippet(type) {
+    const self = this;
+    if (type) {
+      self.selectedSnippet(type);
+    }
+    const snippet = self.addSnippet({
+      type: self.selectedSnippet(),
+      result: {}
+    });
+
+    window.setTimeout(() => {
+      const lastSnippet = snippet;
+      if (lastSnippet.ace() != null) {
+        lastSnippet.ace().focus();
+      }
+    }, 100);
+
+    hueAnalytics.log('notebook', 'add_snippet/' + (type ? type : self.selectedSnippet()));
+    return snippet;
+  }
+
+  newSnippetAbove(id) {
+    const self = this;
+    self.newSnippet();
+    let idx = 0;
+    self.snippets().forEach((snippet, cnt) => {
+      if (snippet.id() === id) {
+        idx = cnt;
+      }
+    });
+    self.snippets(self.snippets().move(self.snippets().length - 1, idx));
+  }
+
+  nextHistoryPage() {
+    const self = this;
+    if (self.historyCurrentPage() < self.historyTotalPages()) {
+      self.historyCurrentPage(self.historyCurrentPage() + 1);
+    }
+  }
+
+  prevHistoryPage() {
+    const self = this;
+    if (self.historyCurrentPage() !== 1) {
+      self.historyCurrentPage(self.historyCurrentPage() - 1);
+    }
+  }
+
+  restartSession(session, callback) {
+    const self = this;
+    if (session.restarting()) {
+      return;
+    }
+    session.restarting(true);
+    const snippets = self.getSnippets(session.type());
+
+    snippets.forEach(snippet => {
+      snippet.status(SNIPPET_STATUS.loading);
+    });
+
+    self.closeSession(session, true, () => {
+      self.createSession(
+        session,
+        () => {
+          snippets.forEach(snippet => {
+            snippet.status(SNIPPET_STATUS.ready);
+          });
+          session.restarting(false);
+          if (callback) {
+            callback();
+          }
+        },
+        () => {
+          session.restarting(false);
+        }
+      );
+    });
+  }
+
+  save(callback) {
+    const self = this;
+    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
+
+    $.post(
+      '/notebook/api/notebook/save',
+      {
+        notebook: komapping.toJSON(cp, NOTEBOOK_MAPPING),
+        editorMode: editorMode
+      },
+      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 (
+              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 (typeof callback == 'function') {
+            callback();
+          }
+        } else {
+          $(document).trigger('error', data.message);
+        }
+      }
+    ).fail(xhr => {
+      if (xhr.status !== 502) {
+        $(document).trigger('error', xhr.responseText);
+      }
+    });
+  }
+
+  saveDefaultUserProperties(session) {
+    const self = this;
+    apiHelper.saveConfiguration({
+      app: session.type(),
+      properties: session.properties,
+      userId: self.parentVm.userId
+    });
+  }
+
+  saveScheduler() {
+    const self = this;
+    if (
+      self.isBatchable() &&
+      (!self.coordinatorUuid() || self.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();
+        }
+      });
+    }
+  }
+
+  showSubmitPopup() {
+    const self = this;
+    $.get(
+      '/oozie/editor/coordinator/submit/' + self.coordinatorUuid(),
+      {
+        format: 'json'
+      },
+      data => {
+        $(document).trigger('showSubmitPopup', data);
+      }
+    ).fail(xhr => {
+      if (xhr.status !== 502) {
+        $(document).trigger('error', xhr.responseText);
+      }
+    });
+  }
+
+  unload() {
+    const self = this;
+    self.unloaded(true);
+    let currentQueries = null;
+    self.snippets().forEach(snippet => {
+      if (snippet.checkStatusTimeout != null) {
+        clearTimeout(snippet.checkStatusTimeout);
+        snippet.checkStatusTimeout = null;
+      }
+      if (currentQueries == null) {
+        currentQueries = snippet.queries();
+      }
+    });
+    return currentQueries;
+  }
+
+  updateHistory(statuses, interval) {
+    const self = this;
+    let items = self
+      .history()
+      .filter(item => statuses.indexOf(item.status()) !== -1)
+      .slice(0, 25);
+
+    const updateHistoryCall = item => {
+      $.post('/notebook/api/check_status', {
+        notebook: komapping.toJSON({ id: item.uuid() })
+      })
+        .done(data => {
+          const status =
+            data.status === -3
+              ? 'expired'
+              : data.status === 0
+              ? data.query_status.status
+              : 'failed';
+          if (status && item.status() !== status) {
+            item.status(status);
+          }
+        })
+        .fail(() => {
+          items = [];
+          self.updateHistoryFailed = true;
+          console.warn('Lost connectivity to the Hue history refresh backend.');
+        })
+        .always(() => {
+          if (items.length > 0) {
+            window.setTimeout(() => {
+              updateHistoryCall(items.pop());
+            }, 1000);
+          } else if (!self.updateHistoryFailed) {
+            window.setTimeout(() => {
+              self.updateHistory(statuses, interval);
+            }, interval);
+          }
+        });
+    };
+
+    if (items.length > 0) {
+      updateHistoryCall(items.pop());
+    } else if (!self.updateHistoryFailed) {
+      window.setTimeout(() => {
+        self.updateHistory(statuses, interval);
+      }, interval);
+    }
+  }
 }
 
 export { Notebook, NOTEBOOK_MAPPING };

+ 138 - 213
desktop/core/src/desktop/js/apps/notebook2/result.js

@@ -19,100 +19,79 @@ import ko from 'knockout';
 
 import hueUtils from 'utils/hueUtils';
 
+const adaptMeta = meta => {
+  meta.forEach((item, index) => {
+    if (typeof item.checked === 'undefined') {
+      item.checked = ko.observable(true);
+      item.checked.subscribe(() => {
+        self.filteredMetaChecked(
+          self.filteredMeta().some(item => {
+            return item.checked();
+          })
+        );
+      });
+    }
+    item.type = item.type.replace(/_type/i, '').toLowerCase();
+    if (typeof item.originalIndex === 'undefined') {
+      item.originalIndex = index;
+    }
+  });
+};
+
+const isNumericColumn = type =>
+  ['tinyint', 'smallint', 'int', 'bigint', 'float', 'double', 'decimal', 'real'].indexOf(type) !==
+  -1;
+
+const isDateTimeColumn = type => ['timestamp', 'date', 'datetime'].indexOf(type) !== -1;
+
+const isComplexColumn = type => ['array', 'map', 'struct'].indexOf(type) !== -1;
+
+const isStringColumn = type =>
+  !isNumericColumn(type) && !isDateTimeColumn(type) && !isComplexColumn(type);
+
 class Result {
   constructor(snippet, result) {
     const self = this;
 
     $.extend(
       snippet,
-      snippet.chartType == 'lines' && {
+      snippet.chartType === 'lines' && {
         // Retire line chart
         chartType: 'bars',
         chartTimelineType: 'line'
       }
     );
-    self.id = ko.observable(
-      typeof result.id != 'undefined' && result.id != null ? result.id : hueUtils.UUID()
-    );
-    self.type = ko.observable(
-      typeof result.type != 'undefined' && result.type != null ? result.type : 'table'
-    );
-    self.hasResultset = ko
-      .observable(
-        typeof result.hasResultset != 'undefined' && result.hasResultset != null
-          ? result.hasResultset
-          : true
-      )
-      .extend('throttle', 100);
-    self.handle = ko.observable(
-      typeof result.handle != 'undefined' && result.handle != null ? result.handle : {}
-    );
-    self.meta = ko.observableArray(
-      typeof result.meta != 'undefined' && result.meta != null ? result.meta : []
-    );
-
-    const adaptMeta = function() {
-      let i = 0;
-      self.meta().forEach(item => {
-        if (typeof item.checked === 'undefined') {
-          item.checked = ko.observable(true);
-          item.checked.subscribe(() => {
-            self.filteredMetaChecked(
-              self.filteredMeta().some(item => {
-                return item.checked();
-              })
-            );
-          });
-        }
-        item.type = item.type.replace(/_type/i, '').toLowerCase();
-        if (typeof item.originalIndex === 'undefined') {
-          item.originalIndex = i;
-        }
-        i++;
-      });
-    };
+    self.id = ko.observable(result.id || hueUtils.UUID());
+    self.type = ko.observable(result.type || 'table');
+    self.hasResultset = ko.observable(result.hasResultset !== false).extend('throttle', 100);
+    self.handle = ko.observable(result.handle || {});
+    self.meta = ko.observableArray(result.meta || []);
 
-    adaptMeta();
-    self.meta.subscribe(adaptMeta);
+    adaptMeta(self.meta());
+    self.meta.subscribe(() => {
+      adaptMeta(self.meta());
+    });
 
-    self.rows = ko.observable(
-      typeof result.rows != 'undefined' && result.rows != null ? result.rows : null
-    );
-    self.hasMore = ko.observable(
-      typeof result.hasMore != 'undefined' && result.hasMore != null ? result.hasMore : false
-    );
-    self.statement_id = ko.observable(
-      typeof result.statement_id != 'undefined' && result.statement_id != null
-        ? result.statement_id
-        : 0
-    );
+    self.rows = ko.observable(result.rows);
+    self.hasMore = ko.observable(!!result.hasMore);
+    self.statement_id = ko.observable(result.statement_id || 0);
     self.statement_range = ko.observable(
-      typeof result.statement_range != 'undefined' && result.statement_range != null
-        ? result.statement_range
-        : {
-            start: {
-              row: 0,
-              column: 0
-            },
-            end: {
-              row: 0,
-              column: 0
-            }
-          }
+      result.statement_range || {
+        start: {
+          row: 0,
+          column: 0
+        },
+        end: {
+          row: 0,
+          column: 0
+        }
+      }
     );
     // We don't keep track of any previous selection so prevent entering into batch execution mode after load by setting
     // statements_count to 1. For the case when a selection is not starting at row 0.
     self.statements_count = ko.observable(1);
-    self.previous_statement_hash = ko.observable(
-      typeof result.previous_statement_hash != 'undefined' && result.previous_statement_hash != null
-        ? result.previous_statement_hash
-        : null
-    );
-    self.cleanedMeta = ko.computed(() => {
-      return ko.utils.arrayFilter(self.meta(), item => {
-        return item.name != '';
-      });
-    });
+    self.previous_statement_hash = ko.observable(result.previous_statement_hash);
+    self.cleanedMeta = ko.pureComputed(() => self.meta().filter(item => item.name !== ''));
     self.metaFilter = ko.observable();
 
     self.isMetaFilterVisible = ko.observable(false);
@@ -141,152 +120,50 @@ class Result {
       });
     });
 
-    self.autocompleteFromEntries = function(nonPartial, partial) {
-      const result = [];
-      const partialLower = partial.toLowerCase();
-      self.meta().forEach(column => {
-        if (column.name.toLowerCase().indexOf(partialLower) === 0) {
-          result.push(nonPartial + partial + column.name.substring(partial.length));
-        } else if (column.name.toLowerCase().indexOf('.' + partialLower) !== -1) {
-          result.push(
-            nonPartial +
-              partial +
-              column.name.substring(
-                partial.length + column.name.toLowerCase().indexOf('.' + partialLower) + 1
-              )
-          );
-        }
-      });
-
-      return result;
-    };
-    self.clickFilteredMetaCheck = function() {
-      self.filteredMeta().forEach(item => {
-        item.checked(self.filteredMetaChecked());
-      });
-    };
-
-    self.fetchedOnce = ko.observable(
-      typeof result.fetchedOnce != 'undefined' && result.fetchedOnce != null
-        ? result.fetchedOnce
-        : false
-    );
-    self.startTime = ko.observable(
-      typeof result.startTime != 'undefined' && result.startTime != null
-        ? new Date(result.startTime)
-        : new Date()
-    );
-    self.endTime = ko.observable(
-      typeof result.endTime != 'undefined' && result.endTime != null
-        ? new Date(result.endTime)
-        : new Date()
+    self.fetchedOnce = ko.observable(!!result.fetchedOnce);
+    self.startTime = ko.observable(result.startTime ? new Date(result.startTime) : new Date());
+    self.endTime = ko.observable(result.endTime ? new Date(result.endTime) : new Date());
+    self.executionTime = ko.pureComputed(
+      () => self.endTime().getTime() - self.startTime().getTime()
     );
-    self.executionTime = ko.computed(() => {
-      return self.endTime().getTime() - self.startTime().getTime();
-    });
-
-    function isNumericColumn(type) {
-      return (
-        $.inArray(type, [
-          'tinyint',
-          'smallint',
-          'int',
-          'bigint',
-          'float',
-          'double',
-          'decimal',
-          'real'
-        ]) > -1
-      );
-    }
-
-    function isDateTimeColumn(type) {
-      return $.inArray(type, ['timestamp', 'date', 'datetime']) > -1;
-    }
-
-    function isComplexColumn(type) {
-      return $.inArray(type, ['array', 'map', 'struct']) > -1;
-    }
-
-    function isStringColumn(type) {
-      return !isNumericColumn(type) && !isDateTimeColumn(type) && !isComplexColumn(type);
-    }
 
-    self.cleanedNumericMeta = ko.computed(() => {
-      return ko.utils.arrayFilter(self.meta(), item => {
-        return item.name != '' && isNumericColumn(item.type);
-      });
-    });
-
-    self.cleanedStringMeta = ko.computed(() => {
-      return ko.utils.arrayFilter(self.meta(), item => {
-        return item.name != '' && isStringColumn(item.type);
-      });
-    });
-
-    self.cleanedDateTimeMeta = ko.computed(() => {
-      return ko.utils.arrayFilter(self.meta(), item => {
-        return item.name != '' && isDateTimeColumn(item.type);
-      });
-    });
-
-    self.data = ko.observableArray(
-      typeof result.data != 'undefined' && result.data != null ? result.data : []
+    self.cleanedNumericMeta = ko.pureComputed(() =>
+      self.meta().filter(item => item.name !== '' && isNumericColumn(item.type))
     );
-    self.data.extend({ rateLimit: 50 });
-    self.explanation = ko.observable(
-      typeof result.explanation != 'undefined' && result.explanation != null
-        ? result.explanation
-        : ''
+    self.cleanedStringMeta = ko.pureComputed(() =>
+      self.meta().filter(item => item.name !== '' && isStringColumn(item.type))
     );
-    self.images = ko.observableArray(
-      typeof result.images != 'undefined' && result.images != null ? result.images : []
+    self.cleanedDateTimeMeta = ko.pureComputed(() =>
+      self.meta().filter(item => item.name !== '' && isDateTimeColumn(item.type))
     );
-    self.images.extend({ rateLimit: 50 });
+
+    self.data = ko.observableArray(result.data || []).extend({ rateLimit: 50 });
+    self.explanation = ko.observable(result.explanation || '');
+    self.images = ko.observableArray(result.images || []).extend({ rateLimit: 50 });
     self.logs = ko.observable('');
     self.logLines = 0;
-    self.hasSomeResults = ko.computed(() => {
-      return self.hasResultset() && self.data().length > 0; // status() == 'available'
-    });
+    self.hasSomeResults = ko.pureComputed(() => self.hasResultset() && self.data().length > 0);
+  }
 
-    self.getContext = function() {
-      return {
-        id: self.id,
-        type: self.type,
-        handle: self.handle
-      };
-    };
+  autocompleteFromEntries(nonPartial, partial) {
+    const self = this;
+    const result = [];
+    const partialLower = partial.toLowerCase();
+    self.meta().forEach(column => {
+      if (column.name.toLowerCase().indexOf(partialLower) === 0) {
+        result.push(nonPartial + partial + column.name.substring(partial.length));
+      } else if (column.name.toLowerCase().indexOf('.' + partialLower) !== -1) {
+        result.push(
+          nonPartial +
+            partial +
+            column.name.substring(
+              partial.length + column.name.toLowerCase().indexOf('.' + partialLower) + 1
+            )
+        );
+      }
+    });
 
-    self.clear = function() {
-      self.fetchedOnce(false);
-      self.hasMore(false);
-      self.statement_range({
-        start: {
-          row: 0,
-          column: 0
-        },
-        end: {
-          row: 0,
-          column: 0
-        }
-      });
-      self.meta.removeAll();
-      self.data.removeAll();
-      self.images.removeAll();
-      self.logs('');
-      self.handle({
-        // Keep multiquery indexing
-        has_more_statements: self.handle()['has_more_statements'],
-        statement_id: self.handle()['statement_id'],
-        statements_count: self.handle()['statements_count'],
-        previous_statement_hash: self.handle()['previous_statement_hash']
-      });
-      self.startTime(new Date());
-      self.endTime(new Date());
-      self.explanation('');
-      self.logLines = 0;
-      self.rows(null);
-    };
+    return result;
   }
 
   cancelBatchExecution() {
@@ -315,6 +192,54 @@ class Result {
     self.handle()['has_more_statements'] = false;
     self.handle()['previous_statement_hash'] = '';
   }
+
+  clear() {
+    const self = this;
+    self.fetchedOnce(false);
+    self.hasMore(false);
+    self.statement_range({
+      start: {
+        row: 0,
+        column: 0
+      },
+      end: {
+        row: 0,
+        column: 0
+      }
+    });
+    self.meta.removeAll();
+    self.data.removeAll();
+    self.images.removeAll();
+    self.logs('');
+    self.handle({
+      // Keep multiquery indexing
+      has_more_statements: self.handle()['has_more_statements'],
+      statement_id: self.handle()['statement_id'],
+      statements_count: self.handle()['statements_count'],
+      previous_statement_hash: self.handle()['previous_statement_hash']
+    });
+    self.startTime(new Date());
+    self.endTime(new Date());
+    self.explanation('');
+    self.logLines = 0;
+    self.rows(null);
+  }
+
+  clickFilteredMetaCheck() {
+    const self = this;
+    self.filteredMeta().forEach(item => {
+      item.checked(self.filteredMetaChecked());
+    });
+  }
+
+  getContext() {
+    const self = this;
+    return {
+      id: self.id,
+      type: self.type,
+      handle: self.handle
+    };
+  }
 }
 
 export default Result;

+ 3 - 7
desktop/core/src/desktop/js/apps/notebook2/session.js

@@ -14,7 +14,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import $ from 'jquery';
 import ko from 'knockout';
 import komapping from 'knockout.mapping';
 
@@ -31,15 +30,12 @@ class Session {
       self.properties = ko.observableArray();
     }
 
-    self.availableNewProperties = ko.computed(() => {
+    self.availableNewProperties = ko.pureComputed(() => {
       const addedIndex = {};
-      $.each(self.properties(), (index, property) => {
+      self.properties().forEach(property => {
         addedIndex[property.key] = true;
       });
-      const result = $.grep(vm.availableSessionProperties(), property => {
-        return !addedIndex[property.name];
-      });
-      return result;
+      return vm.availableSessionProperties().filter(property => !addedIndex[property.name]);
     });
   }
 }

+ 3 - 3
desktop/core/src/desktop/js/apps/notebook2/snippet.js

@@ -1331,7 +1331,7 @@ class Snippet {
       self.lastAceSelectionRowOffset(Math.min(selectionRange.start.row, selectionRange.end.row));
     }
 
-    self.previousChartOptions = self.parentVm._getPreviousChartOptions(self);
+    self.previousChartOptions = self.parentVm.getPreviousChartOptions(self);
     $(document).trigger('executeStarted', { vm: self.parentVm, snippet: self });
     self.lastExecuted(now);
     $('.jHueNotify').remove();
@@ -1437,7 +1437,7 @@ class Snippet {
               }
             } else {
               self.parentNotebook.history.unshift(
-                self.parentNotebook._makeHistoryRecord(
+                self.parentNotebook.makeHistoryRecord(
                   undefined,
                   data.handle.statement,
                   self.lastExecuted(),
@@ -2577,4 +2577,4 @@ class Snippet {
   }
 }
 
-export default Snippet;
+export { Snippet, STATUS };

+ 1 - 1
desktop/core/src/desktop/js/ko/components/ko.historyPanel.js

@@ -279,7 +279,7 @@ class HistoryPanel {
 
         // Add to history
         notebook.history.unshift(
-          notebook._makeHistoryRecord(
+          notebook.makeHistoryRecord(
             notebook.onSuccessUrl(),
             notebook.description(),
             new Date().getTime(),