Browse Source

HUE-9000 [editor] Add save/load logic for executables in notebook 2

Johan Ahlen 6 years ago
parent
commit
0a009304a1

+ 40 - 13
desktop/core/src/desktop/js/api/apiHelper.js

@@ -22,6 +22,7 @@ import CancellablePromise from 'api/cancellablePromise';
 import hueDebug from 'utils/hueDebug';
 import huePubSub from 'utils/huePubSub';
 import hueUtils from 'utils/hueUtils';
+import { EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
 
 const AUTOCOMPLETE_API_PREFIX = '/notebook/api/autocomplete/';
 const SAMPLE_API_PREFIX = '/notebook/api/sample/';
@@ -1256,33 +1257,34 @@ class ApiHelper {
   /**
    * @param {Object} options
    * @param {number} options.uuid
+   * @param {boolean} [options.dependencies]
    * @param {boolean} [options.silenceErrors]
    * @param {boolean} [options.fetchContents]
    *
    * @return {CancellablePromise}
    */
   fetchDocument(options) {
-    const self = this;
     const deferred = $.Deferred();
     const request = $.ajax({
       url: DOCUMENTS_API,
       data: {
         uuid: options.uuid,
-        data: !!options.fetchContents
+        data: !!options.fetchContents,
+        dependencies: options.dependencies
       },
-      success: function(data) {
-        if (!self.successResponseIsError(data)) {
+      success: data => {
+        if (!this.successResponseIsError(data)) {
           deferred.resolve(data);
         } else {
           deferred.reject(
-            self.assistErrorCallback({
+            this.assistErrorCallback({
               silenceErrors: options.silenceErrors
             })
           );
         }
       }
     }).fail(
-      self.assistErrorCallback({
+      this.assistErrorCallback({
         silenceErrors: options.silenceErrors,
         errorCallback: deferred.reject
       })
@@ -1290,6 +1292,24 @@ class ApiHelper {
     return new CancellablePromise(deferred, request);
   }
 
+  /**
+   * @param {Object} options
+   * @param {number} options.uuid
+   * @param {boolean} [options.silenceErrors]
+   * @param {boolean} [options.dependencies]
+   * @param {boolean} [options.fetchContents]
+   *
+   * @param options
+   * @return {Promise<unknown>}
+   */
+  async fetchDocumentAsync(options) {
+    return new Promise((resolve, reject) => {
+      this.fetchDocument(options)
+        .done(resolve)
+        .fail(reject);
+    });
+  }
+
   /**
    * @param {Object} options
    * @param {Function} options.successCallback
@@ -2121,15 +2141,22 @@ class ApiHelper {
   checkExecutionStatus(options) {
     const deferred = $.Deferred();
 
-    const request = this.simplePost(
-      '/notebook/api/check_status',
-      ApiHelper.adaptExecutableToNotebook(options.executable),
-      options
-    )
+    const request = $.post({
+      url: '/notebook/api/check_status',
+      data: ApiHelper.adaptExecutableToNotebook(options.executable)
+    })
       .done(response => {
-        deferred.resolve(response.query_status.status);
+        if (response && response.query_status) {
+          deferred.resolve(response.query_status.status);
+        } else if (response && response.status === -3) {
+          deferred.resolve(EXECUTION_STATUS.expired);
+        } else {
+          deferred.resolve(EXECUTION_STATUS.failed);
+        }
       })
-      .fail(deferred.reject);
+      .fail(err => {
+        deferred.reject(this.assistErrorCallback(options)(err));
+      });
 
     return new CancellablePromise(deferred, request);
   }

+ 64 - 0
desktop/core/src/desktop/js/apps/notebook2/components/executableStateHandler.js

@@ -0,0 +1,64 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+export const attachTracker = (activeExecutable, id, target, trackedObservables) => {
+  const disposals = [];
+  if (!activeExecutable) {
+    return;
+  }
+
+  let ignoreObservableChange = false;
+
+  const updateFromState = state => {
+    ignoreObservableChange = true;
+    Object.keys(state).forEach(key => target[key](state[key]));
+    ignoreObservableChange = false;
+  };
+
+  updateFromState(trackedObservables);
+
+  const sub = activeExecutable.subscribe(executable => {
+    const state = Object.assign({}, trackedObservables, executable.observerState[id]);
+    updateFromState(state);
+  });
+
+  disposals.push(() => {
+    sub.dispose();
+  });
+
+  Object.keys(trackedObservables).forEach(observableAttr => {
+    const sub = target[observableAttr].subscribe(val => {
+      if (ignoreObservableChange) {
+        return;
+      }
+      if (!activeExecutable().observerState[id]) {
+        activeExecutable().observerState[id] = {};
+      }
+      activeExecutable().observerState[id][observableAttr] = val;
+    });
+    disposals.push(() => {
+      sub.dispose();
+    });
+  });
+
+  return {
+    dispose: () => {
+      while (disposals.length) {
+        disposals.pop()();
+      }
+    }
+  };
+};

+ 12 - 2
desktop/core/src/desktop/js/apps/notebook2/components/ko.snippetResults.js

@@ -27,6 +27,7 @@ import { REDRAW_FIXED_HEADERS_EVENT } from 'apps/notebook2/events';
 import { REDRAW_CHART_EVENT } from 'apps/notebook2/events';
 import { EXECUTABLE_UPDATED_EVENT } from 'apps/notebook2/execution/executable';
 import { RESULT_TYPE, RESULT_UPDATED_EVENT } from 'apps/notebook2/execution/executionResult';
+import { attachTracker } from 'apps/notebook2/components/executableStateHandler';
 
 export const NAME = 'snippet-results';
 
@@ -95,6 +96,7 @@ const TEMPLATE = `
         <!-- ko component: { 
           name: 'result-grid',
           params: {
+            activeExecutable: activeExecutable,
             data: data,
             lastFetchedRows: lastFetchedRows,
             editorMode: editorMode,
@@ -112,6 +114,7 @@ const TEMPLATE = `
         <!-- ko component: {
           name: 'result-chart',
           params: {
+            activeExecutable: activeExecutable,
             cleanedMeta: cleanedMeta,
             cleanedDateTimeMeta: cleanedDateTimeMeta,
             cleanedNumericMeta: cleanedNumericMeta,
@@ -155,8 +158,15 @@ class SnippetResults extends DisposableComponent {
 
     this.hasSomeResult = ko.pureComputed(() => this.data().length);
 
-    this.showGrid = ko.observable(true); // TODO: Should be persisted
-    this.showChart = ko.observable(false); // TODO: Should be persisted
+    const trackedObservables = {
+      showGrid: true,
+      showChart: false
+    };
+
+    this.showGrid = ko.observable(trackedObservables.showGrid);
+    this.showChart = ko.observable(trackedObservables.showChart);
+
+    attachTracker(this.activeExecutable, NAME, this, trackedObservables);
 
     this.cleanedMeta = ko.pureComputed(() => this.meta().filter(item => item.name !== ''));
 

+ 37 - 15
desktop/core/src/desktop/js/apps/notebook2/components/resultChart/ko.resultChart.js

@@ -31,6 +31,7 @@ import {
 import $ from 'jquery';
 import { UUID } from 'utils/hueUtils';
 import { REDRAW_CHART_EVENT } from 'apps/notebook2/events';
+import { attachTracker } from 'apps/notebook2/components/executableStateHandler';
 
 export const NAME = 'result-chart';
 
@@ -495,7 +496,7 @@ class ResultChart extends DisposableComponent {
 
     this.data = params.data;
     this.id = params.id;
-    this.chartSettingsVisible = ko.observable(true);
+    this.activeExecutable = params.activeExecutable;
 
     this.meta = params.meta;
     this.cleanedMeta = params.cleanedMeta;
@@ -504,21 +505,42 @@ class ResultChart extends DisposableComponent {
     this.cleanedStringMeta = params.cleanedNumericMeta;
     this.showChart = params.showChart;
 
-    this.chartLimit = ko.observable().extend({ notify: 'always' }); // TODO: Should be persisted
+    const trackedObservables = {
+      chartLimit: undefined,
+      chartMapHeat: undefined,
+      chartMapLabel: undefined,
+      chartMapType: CHART_MAP_TYPE.MARKER,
+      chartScatterGroup: undefined,
+      chartScatterSize: undefined,
+      chartScope: CHART_SCOPE.WORLD,
+      chartSettingsVisible: true,
+      chartSorting: CHART_SORTING.NONE,
+      chartTimelineType: CHART_TIMELINE_TYPE.BAR,
+      chartX: undefined,
+      chartXPivot: undefined,
+      chartYMulti: [],
+      chartYSingle: undefined,
+      chartType: window.HUE_CHARTS.TYPES.BARCHART
+    };
+
+    this.chartLimit = ko.observable(trackedObservables.chartLimit).extend({ notify: 'always' });
     this.chartLimits = ko.observableArray([5, 10, 25, 50, 100]);
-    this.chartMapHeat = ko.observable(); // TODO: Should be persisted
-    this.chartMapLabel = ko.observable(); // TODO: Should be persisted
-    this.chartMapType = ko.observable(CHART_MAP_TYPE.MARKER); // TODO: Should be persisted
-    this.chartScatterGroup = ko.observable(); // TODO: Should be persisted
-    this.chartScatterSize = ko.observable(); // TODO: Should be persisted
-    this.chartScope = ko.observable(CHART_SCOPE.WORLD); // TODO: Should be persisted
-    this.chartSorting = ko.observable(CHART_SORTING.NONE); // TODO: Should be persisted
-    this.chartTimelineType = ko.observable(CHART_TIMELINE_TYPE.BAR); // TODO: Should be persisted
-    this.chartX = ko.observable().extend({ notify: 'always' }); // TODO: Should be persisted
-    this.chartXPivot = ko.observable().extend({ notify: 'always' }); // TODO: Should be persisted
-    this.chartYMulti = ko.observableArray(); // TODO: Should be persisted
-    this.chartYSingle = ko.observable(); // TODO: Should be persisted
-    this.chartType = ko.observable(window.HUE_CHARTS.TYPES.BARCHART); // TODO: Should be persisted
+    this.chartMapHeat = ko.observable(trackedObservables.chartMapHeat);
+    this.chartMapLabel = ko.observable(trackedObservables.chartMapLabel);
+    this.chartMapType = ko.observable(trackedObservables.chartMapType);
+    this.chartScatterGroup = ko.observable(trackedObservables.chartScatterGroup);
+    this.chartScatterSize = ko.observable(trackedObservables.chartScatterSize);
+    this.chartScope = ko.observable(trackedObservables.chartScope);
+    this.chartSettingsVisible = ko.observable(trackedObservables.chartSettingsVisible);
+    this.chartSorting = ko.observable(trackedObservables.chartSorting);
+    this.chartTimelineType = ko.observable(trackedObservables.chartTimelineType);
+    this.chartX = ko.observable(trackedObservables.chartX).extend({ notify: 'always' });
+    this.chartXPivot = ko.observable(trackedObservables.chartXPivot).extend({ notify: 'always' });
+    this.chartYMulti = ko.observableArray(trackedObservables.chartYMulti);
+    this.chartYSingle = ko.observable(trackedObservables.chartYSingle);
+    this.chartType = ko.observable(trackedObservables.chartType);
+
+    attachTracker(this.activeExecutable, NAME, this, trackedObservables);
 
     this.chartId = ko.pureComputed(() => this.chartType() + '_' + this.id());
     this.isBarChart = ko.pureComputed(() => TYPES.BARCHART === this.chartType());

+ 12 - 2
desktop/core/src/desktop/js/apps/notebook2/components/resultGrid/ko.resultGrid.js

@@ -28,6 +28,7 @@ import {
   SHOW_GRID_SEARCH_EVENT,
   SHOW_NORMAL_RESULT_EVENT
 } from 'apps/notebook2/events';
+import { attachTracker } from 'apps/notebook2/components/executableStateHandler';
 
 export const NAME = 'result-grid';
 
@@ -169,6 +170,7 @@ class ResultGrid extends DisposableComponent {
   constructor(params, element) {
     super();
     this.element = element;
+    this.activeExecutable = params.activeExecutable;
 
     this.isResultFullScreenMode = params.isResultFullScreenMode;
     this.editorMode = params.editorMode;
@@ -176,15 +178,23 @@ class ResultGrid extends DisposableComponent {
     this.hasMore = params.hasMore;
     this.fetchResult = params.fetchResult;
 
+    const trackedObservables = {
+      columnsVisible: true,
+      isMetaFilterVisible: false
+    };
+
     this.status = params.status;
-    this.columnsVisible = ko.observable(true);
-    this.isMetaFilterVisible = ko.observable(false);
+    this.columnsVisible = ko.observable(trackedObservables.columnsVisible);
+    this.isMetaFilterVisible = ko.observable(trackedObservables.isMetaFilterVisible);
     this.metaFilter = ko.observable();
     this.resultsKlass = params.resultsKlass;
     this.meta = params.meta;
     this.data = params.data;
     this.lastFetchedRows = params.lastFetchedRows;
 
+    const tracker = attachTracker(this.activeExecutable, NAME, this, trackedObservables);
+    super.addDisposalCallback(tracker.dispose.bind(tracker));
+
     this.hueDatatable = undefined;
 
     this.subscribe(this.columnsVisible, () => {

+ 33 - 36
desktop/core/src/desktop/js/apps/notebook2/editorViewModel.js

@@ -72,8 +72,7 @@ class EditorViewModel {
       () =>
         this.selectedNotebook() &&
         this.selectedNotebook().snippets().length === 1 &&
-        this
-          .selectedNotebook()
+        this.selectedNotebook()
           .snippets()[0]
           .isSqlDialect()
     );
@@ -251,8 +250,7 @@ class EditorViewModel {
       this.combinedContent('');
     } else {
       let statements = '';
-      this
-        .selectedNotebook()
+      this.selectedNotebook()
         .snippets()
         .forEach(snippet => {
           if (snippet.statement()) {
@@ -397,42 +395,41 @@ class EditorViewModel {
   }
 
   async openNotebook(uuid, queryTab, skipUrlChange, callback) {
-    return new Promise((resolve, reject) => {
-      $.get('/desktop/api2/doc/', {
+    try {
+      const docData = await apiHelper.fetchDocumentAsync({
         uuid: uuid,
-        data: true,
+        fetchContents: true,
         dependencies: true
-      }).then(data => {
-        if (data.status === 0) {
-          data.data.dependents = data.dependents;
-          data.data.can_write = data.user_perms.can_write;
-          const notebookRaw = data.data;
-          this.loadNotebook(notebookRaw, queryTab);
-          if (typeof skipUrlChange === 'undefined' && !this.isNotificationManager()) {
-            if (this.editorMode()) {
-              this.editorType(data.document.type.substring('query-'.length));
-              huePubSub.publish('active.snippet.type.changed', {
-                type: this.editorType(),
-                isSqlDialect: this.getSnippetViewSettings(this.editorType()).sqlDialect
-              });
-              this.changeURL(
-                this.URLS.editor + '?editor=' + data.document.id + '&type=' + this.editorType()
-              );
-            } else {
-              this.changeURL(this.URLS.notebook + '?notebook=' + data.document.id);
-            }
-          }
-          if (typeof callback !== 'undefined') {
-            callback();
-          }
-          resolve();
+      });
+
+      docData.data.dependents = docData.dependents;
+      docData.data.can_write = docData.user_perms.can_write;
+
+      const notebookRaw = docData.data;
+
+      this.loadNotebook(notebookRaw, queryTab);
+
+      if (typeof skipUrlChange === 'undefined' && !this.isNotificationManager()) {
+        if (this.editorMode()) {
+          this.editorType(docData.document.type.substring('query-'.length));
+          huePubSub.publish('active.snippet.type.changed', {
+            type: this.editorType(),
+            isSqlDialect: this.getSnippetViewSettings(this.editorType()).sqlDialect
+          });
+          this.changeURL(
+            this.URLS.editor + '?editor=' + docData.document.id + '&type=' + this.editorType()
+          );
         } else {
-          $(document).trigger('error', data.message);
-          reject();
-          this.newNotebook();
+          this.changeURL(this.URLS.notebook + '?notebook=' + docData.document.id);
         }
-      });
-    });
+      }
+      if (typeof callback !== 'undefined') {
+        callback();
+      }
+    } catch (err) {
+      console.error(err);
+      await this.newNotebook();
+    }
   }
 
   prepareShareModal() {

+ 18 - 1
desktop/core/src/desktop/js/apps/notebook2/execution/executable.js

@@ -222,7 +222,7 @@ export default class Executable {
 
     this.cancellables.push(
       apiHelper.checkExecutionStatus({ executable: this }).done(async queryStatus => {
-        switch (this.status) {
+        switch (queryStatus) {
           case EXECUTION_STATUS.success:
             this.executeEnded = Date.now();
             this.setStatus(queryStatus);
@@ -308,6 +308,23 @@ export default class Executable {
     this.setStatus(EXECUTION_STATUS.ready);
   }
 
+  toJs() {
+    const state = Object.assign({}, this.observerState);
+    delete state.aceAnchor;
+
+    return {
+      type: 'executable',
+      handle: this.handle,
+      status: this.status,
+      progress: this.progress,
+      logs: this.logs.toJs(),
+      executeStarted: this.executeStarted,
+      executeEnded: this.executeEnded,
+      observerState: state,
+      lost: this.lost
+    };
+  }
+
   async close() {
     while (this.cancellables.length) {
       const nextCancellable = this.cancellables.pop();

+ 7 - 0
desktop/core/src/desktop/js/apps/notebook2/execution/executionLogs.js

@@ -85,4 +85,11 @@ export default class ExecutionLogs {
       });
     }
   }
+
+  toJs() {
+    return {
+      jobs: this.jobs,
+      errors: this.errors
+    };
+  }
 }

+ 0 - 1
desktop/core/src/desktop/js/apps/notebook2/execution/executionResult.js

@@ -34,7 +34,6 @@ export default class ExecutionResult {
     this.executable = executable;
 
     this.type = RESULT_TYPE.TABLE;
-    this.state = {};
     this.rows = [];
     this.meta = [];
     this.lastRows = [];

+ 6 - 0
desktop/core/src/desktop/js/apps/notebook2/execution/executor.js

@@ -85,6 +85,12 @@ class Executor {
     };
   }
 
+  toJs() {
+    return {
+      executables: this.executables.map(executable => executable.toJs())
+    };
+  }
+
   update(statementDetails, beforeExecute) {
     const executables = this.getExecutables(statementDetails);
 

+ 26 - 0
desktop/core/src/desktop/js/apps/notebook2/execution/sqlExecutable.js

@@ -52,4 +52,30 @@ export default class SqlExecutable extends Executable {
   canExecuteInBatch() {
     return this.parsedStatement && BATCHABLE_STATEMENT_TYPES.test(this.parsedStatement.firstToken);
   }
+
+  static fromJs(executor, executableRaw) {
+    const executable = new SqlExecutable({
+      executor: executor,
+      database: executableRaw.database,
+      parsedStatement: executableRaw.parsedStatement
+    });
+    executable.observerState = executableRaw.observerState || {};
+    executable.progress = executableRaw.progress;
+    executable.status = executableRaw.status;
+    executable.handle = executableRaw.handle;
+    executable.lost = executableRaw.lost;
+    executable.logs.jobs = executableRaw.logs.jobs;
+    executable.logs.errors = executableRaw.logs.errors;
+    executable.executeStarted = executableRaw.executeStarted;
+    executable.executeEnded = executableRaw.executeEnded;
+    return executable;
+  }
+
+  toJs() {
+    const executableJs = super.toJs();
+    executableJs.type = 'sqlExecutable';
+    executableJs.database = this.database;
+    executableJs.parsedStatement = this.parsedStatement;
+    return executableJs;
+  }
 }

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

@@ -24,7 +24,7 @@ import huePubSub from 'utils/huePubSub';
 import hueUtils from 'utils/hueUtils';
 
 import Snippet, { STATUS as SNIPPET_STATUS } from 'apps/notebook2/snippet';
-import { notebookToContextJSON, notebookToJSON } from 'apps/notebook2/notebookSerde';
+import { notebookToContextJSON } from 'apps/notebook2/notebookSerde';
 
 export default class Notebook {
   constructor(vm, notebook) {
@@ -261,7 +261,7 @@ export default class Notebook {
   close() {
     hueAnalytics.log('notebook', 'close');
     apiHelper.closeNotebook({
-      notebookJson: notebookToJSON(this),
+      notebookJson: this.toJson(),
       editorMode: this.parentVm.editorMode()
     });
   }
@@ -460,7 +460,7 @@ export default class Notebook {
 
     try {
       const data = await apiHelper.saveNotebook({
-        notebookJson: notebookToJSON(this),
+        notebookJson: this.toJson(),
         editorMode: editorMode
       });
 
@@ -520,6 +520,7 @@ export default class Notebook {
         $(document).trigger('error', data.message);
       }
     } catch (err) {
+      console.error(err);
       if (err && err.status !== 502) {
         $(document).trigger('error', err.responseText);
       }
@@ -558,6 +559,37 @@ export default class Notebook {
     });
   }
 
+  toJs() {
+    return {
+      coordinatorUuid: this.coordinatorUuid(),
+      description: this.description(),
+      directoryUuid: this.directoryUuid(),
+      executingAllIndex: this.executingAllIndex(),
+      id: this.id(),
+      isExecutingAll: this.isExecutingAll(),
+      isHidingCode: this.isHidingCode(),
+      isHistory: this.isHistory(),
+      isManaged: this.isManaged(),
+      isPresentationModeDefault: this.isPresentationModeDefault(),
+      isSaved: this.isSaved(),
+      name: this.name(),
+      onSuccessUrl: this.onSuccessUrl(),
+      parentSavedQueryUuid: this.parentSavedQueryUuid(),
+      presentationSnippets: this.presentationSnippets(),
+      pubSubUrl: this.pubSubUrl(),
+      result: {}, // TODO: Moved to executor but backend requires it
+      sessions: [], // TODO: Moved to executor but backend requires it
+      snippets: this.snippets().map(snippet => snippet.toJs()),
+      type: this.type(),
+      uuid: this.uuid(),
+      viewSchedulerId: this.viewSchedulerId()
+    };
+  }
+
+  toJson() {
+    return JSON.stringify(this.toJs());
+  }
+
   unload() {
     this.unloaded(true);
     let currentQueries = null;

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

@@ -42,95 +42,3 @@ export const notebookToContextJSON = notebook =>
     type: notebook.type(),
     uuid: notebook.uuid()
   });
-
-export const notebookToJSON = notebook =>
-  JSON.stringify({
-    coordinatorUuid: notebook.coordinatorUuid(),
-    description: notebook.description(),
-    directoryUuid: notebook.directoryUuid(),
-    executingAllIndex: notebook.executingAllIndex(),
-    id: notebook.id(),
-    isExecutingAll: notebook.isExecutingAll(),
-    isHidingCode: notebook.isHidingCode(),
-    isHistory: notebook.isHistory(),
-    isManaged: notebook.isManaged(),
-    isPresentationModeDefault: notebook.isPresentationModeDefault(),
-    isSaved: notebook.isSaved(),
-    name: notebook.name(),
-    onSuccessUrl: notebook.onSuccessUrl(),
-    parentSavedQueryUuid: notebook.parentSavedQueryUuid(),
-    presentationSnippets: notebook.presentationSnippets(),
-    pubSubUrl: notebook.pubSubUrl(),
-    result: {}, // TODO: Moved to executor but backend requires it
-    sessions: [], // TODO: Moved to executor but backend requires it
-    snippets: notebook.snippets().map(snippet => ({
-      aceCursorPosition: snippet.aceCursorPosition(),
-      aceSize: snippet.aceSize(),
-      associatedDocumentUuid: snippet.associatedDocumentUuid(),
-      // chartLimit: snippet.chartLimit(), // TODO: Move somewhere else
-      // chartMapHeat: snippet.chartMapHeat(), // TODO: Move somewhere else
-      // chartMapLabel: snippet.chartMapLabel(), // TODO: Move somewhere else
-      // chartMapType: snippet.chartMapType(), // TODO: Move somewhere else
-      // chartScatterGroup: snippet.chartScatterGroup(), // TODO: Move somewhere else
-      // chartScatterSize: snippet.chartScatterSize(), // TODO: Move somewhere else
-      // chartScope: snippet.chartScope(), // TODO: Move somewhere else
-      // chartSorting: snippet.chartSorting(), // TODO: Move somewhere else
-      // chartTimelineType: snippet.chartTimelineType(), // TODO: Move somewhere else
-      //
-      // $.extend(
-      //   snippet,
-      //   snippet.chartType === 'lines' && {
-      //     // Retire line chart
-      //     chartType: 'bars',
-      //     chartTimelineType: 'line'
-      //   }
-      // );
-      //
-      // chartType: snippet.chartType(), // TODO: Move somewhere else
-      // chartX: snippet.chartX(), // TODO: Move somewhere else
-      // chartXPivot: snippet.chartXPivot(), // TODO: Move somewhere else
-      // chartYMulti: snippet.chartYMulti(), // TODO: Move somewhere else
-      // chartYSingle: snippet.chartYSingle(), // TODO: Move somewhere else
-      compute: snippet.compute(),
-      currentQueryTab: snippet.currentQueryTab(),
-      database: snippet.database(),
-      id: snippet.id(),
-      is_redacted: snippet.is_redacted(),
-      //isResultSettingsVisible: snippet.isResultSettingsVisible(), // TODO: Move somewhere else
-      lastAceSelectionRowOffset: snippet.lastAceSelectionRowOffset(),
-      lastExecuted: snippet.lastExecuted(),
-      name: snippet.name(),
-      namespace: snippet.namespace(),
-      pinnedContextTabs: snippet.pinnedContextTabs(),
-      properties: komapping.toJS(snippet.properties), // TODO: Drop komapping
-      // result: ...,
-      settingsVisible: snippet.settingsVisible(),
-      // schedulerViewModel: ?
-      // showChart: snippet.showChart(), // TODO: Move somewhere else
-      // showGrid: snippet.showGrid(), // TODO: Move somewhere else
-      showLogs: snippet.showLogs(),
-      statement_raw: snippet.statement_raw(),
-      statementPath: snippet.statementPath(),
-      statementType: snippet.statementType(),
-      status: snippet.status(),
-      type: snippet.type(),
-      variables: snippet.variables().map(variable => ({
-        meta: variable.meta && {
-          options: variable.meta.options && variable.meta.options(), // TODO: Map?
-          placeHolder: variable.meta.placeHolder && variable.meta.placeHolder(),
-          type: variable.meta.type && variable.meta.type()
-        },
-        name: variable.name(),
-        path: variable.path(),
-        sample: variable.sample(),
-        sampleUser: variable.sampleUser(),
-        step: variable.step(),
-        type: variable.type(),
-        value: variable.value()
-      })),
-      wasBatchExecuted: snippet.wasBatchExecuted()
-    })),
-    type: notebook.type(),
-    uuid: notebook.uuid(),
-    viewSchedulerId: notebook.viewSchedulerId()
-  });

+ 74 - 7
desktop/core/src/desktop/js/apps/notebook2/snippet.js

@@ -35,14 +35,14 @@ import sessionManager from 'apps/notebook2/execution/sessionManager';
 import SqlExecutable from 'apps/notebook2/execution/sqlExecutable';
 import { notebookToContextJSON, snippetToContextJSON } from 'apps/notebook2/notebookSerde';
 import { REDRAW_FIXED_HEADERS_EVENT } from 'apps/notebook2/events';
-import { EXECUTABLE_UPDATED_EVENT } from 'apps/notebook2/execution/executable';
+import { EXECUTABLE_UPDATED_EVENT, EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
 import {
   ACTIVE_STATEMENT_CHANGED_EVENT,
   REFRESH_STATEMENT_LOCATIONS_EVENT
 } from 'ko/bindings/ace/aceLocationHandler';
 import { EXECUTE_ACTIVE_EXECUTABLE_EVENT } from 'apps/notebook2/components/ko.executableActions';
 
-// TODO: Remove. Temporary here for debug
+// TODO: Remove for ENABLE_NOTEBOOK_2. Temporary here for debug
 window.SqlExecutable = SqlExecutable;
 window.Executor = Executor;
 
@@ -397,15 +397,15 @@ export default class Snippet {
           this.activeExecutable(this.executor.update(statementDetails, beforeExecute));
           beforeExecute = false;
           if (statementDetails.activeStatement) {
-            const _statements = [];
+            const statementsList = [];
             statementDetails.precedingStatements.forEach(statement => {
-              _statements.push(statement.statement);
+              statementsList.push(statement.statement);
             });
-            _statements.push(statementDetails.activeStatement.statement);
+            statementsList.push(statementDetails.activeStatement.statement);
             statementDetails.followingStatements.forEach(statement => {
-              _statements.push(statement.statement);
+              statementsList.push(statement.statement);
             });
-            this.statementsList(_statements); // Or fetch on demand via editor.refresh.statement.locations and remove observableArray?
+            this.statementsList(statementsList); // Or fetch on demand via editor.refresh.statement.locations and remove observableArray?
           } else {
             this.statementsList([]);
           }
@@ -1008,6 +1008,31 @@ export default class Snippet {
       isSqlEngine: this.isSqlDialect
     });
 
+    if (snippet.executor) {
+      try {
+        this.executor.executables = snippet.executor.executables.map(executableRaw => {
+          switch (executableRaw.type) {
+            case 'sqlExecutable': {
+              return SqlExecutable.fromJs(this.executor, executableRaw);
+            }
+            default: {
+              throw new Error('Failed to created executable of type ' + executableRaw.type);
+            }
+          }
+        });
+
+        this.executor.executables.forEach(async executable => {
+          if (executable.status !== EXECUTION_STATUS.ready) {
+            await executable.checkStatus();
+          } else {
+            executable.notify();
+          }
+        });
+      } catch (err) {
+        console.error(err); // TODO: Move up
+      }
+    }
+
     huePubSub.subscribe(EXECUTABLE_UPDATED_EVENT, executable => {
       if (this.activeExecutable() === executable) {
         this.updateFromExecutable();
@@ -1475,6 +1500,48 @@ export default class Snippet {
     this.showLongOperationWarning(false);
   }
 
+  toJs() {
+    return {
+      aceCursorPosition: this.aceCursorPosition(),
+      aceSize: this.aceSize(),
+      associatedDocumentUuid: this.associatedDocumentUuid(),
+      executor: this.executor.toJs(),
+      compute: this.compute(),
+      currentQueryTab: this.currentQueryTab(),
+      database: this.database(),
+      id: this.id(),
+      is_redacted: this.is_redacted(),
+      lastAceSelectionRowOffset: this.lastAceSelectionRowOffset(),
+      lastExecuted: this.lastExecuted(),
+      name: this.name(),
+      namespace: this.namespace(),
+      pinnedContextTabs: this.pinnedContextTabs(),
+      properties: komapping.toJS(this.properties), // TODO: Drop komapping
+      settingsVisible: this.settingsVisible(),
+      showLogs: this.showLogs(),
+      statement_raw: this.statement_raw(),
+      statementPath: this.statementPath(),
+      statementType: this.statementType(),
+      status: this.status(),
+      type: this.type(),
+      variables: this.variables().map(variable => ({
+        meta: variable.meta && {
+          options: variable.meta.options && variable.meta.options(), // TODO: Map?
+          placeHolder: variable.meta.placeHolder && variable.meta.placeHolder(),
+          type: variable.meta.type && variable.meta.type()
+        },
+        name: variable.name(),
+        path: variable.path(),
+        sample: variable.sample(),
+        sampleUser: variable.sampleUser(),
+        step: variable.step(),
+        type: variable.type(),
+        value: variable.value()
+      })),
+      wasBatchExecuted: this.wasBatchExecuted()
+    };
+  }
+
   uploadQuery(query_id) {
     $.post('/metadata/api/optimizer/upload/query', {
       query_id: query_id,

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

@@ -14,7 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { notebookToContextJSON, notebookToJSON, snippetToContextJSON } from '../notebookSerde';
+import { notebookToContextJSON, snippetToContextJSON } from '../notebookSerde';
 import Notebook from '../notebook';
 import sessionManager from 'apps/notebook2/execution/sessionManager';
 
@@ -42,7 +42,7 @@ describe('notebookSerde.js', () => {
     notebook.addSnippet({ type: 'hive' });
     notebook.addSnippet({ type: 'impala' });
 
-    const notebookJSON = notebookToJSON(notebook);
+    const notebookJSON = notebook.toJson();
 
     const notebookRaw = JSON.parse(notebookJSON);
 

+ 3 - 4
desktop/core/src/desktop/js/ko/bindings/ace/aceAnchoredRange.js

@@ -22,7 +22,7 @@ const clearGutterCss = (cssClass, session, startRow, endRow) => {
   }
 };
 
-const setGuttercss = (cssClass, session, startRow, endRow) => {
+const setGutterCss = (cssClass, session, startRow, endRow) => {
   for (let row = startRow; row <= endRow; row++) {
     session.addGutterDecoration(row, cssClass);
   }
@@ -30,7 +30,6 @@ const setGuttercss = (cssClass, session, startRow, endRow) => {
 
 export default class AceAnchoredRange {
   constructor(editor) {
-    this.id = Math.random();
     this.editor = editor;
     const doc = this.editor.getSession().doc;
     this.startAnchor = doc.createAnchor(0, 0);
@@ -64,7 +63,7 @@ export default class AceAnchoredRange {
       clearGutterCss(cssClass, session, rowSpan.start, rowSpan.end);
       rowSpan.start = newStart.row;
       rowSpan.end = newEnd.row;
-      setGuttercss(cssClass, session, rowSpan.start, rowSpan.end);
+      setGutterCss(cssClass, session, rowSpan.start, rowSpan.end);
     });
   }
 
@@ -81,7 +80,7 @@ export default class AceAnchoredRange {
     const startRow = this.startAnchor.getPosition().row;
     const endRow = this.endAnchor.getPosition().row;
     this.gutterCssClasses[cssClass] = { start: startRow, end: endRow };
-    setGuttercss(cssClass, session, startRow, endRow);
+    setGutterCss(cssClass, session, startRow, endRow);
   }
 
   addMarkerCss(cssClass) {

+ 7 - 7
desktop/core/src/desktop/js/ko/bindings/ace/aceGutterHandler.js

@@ -76,20 +76,20 @@ export default class AceGutterHandler {
       const executableSub = huePubSub.subscribe(EXECUTABLE_UPDATED_EVENT, executable => {
         if (executable.executor === this.executor) {
           if (executable.lost) {
-            if (executable.obseverState.aceAnchor) {
-              executable.obseverState.aceAnchor.dispose();
-              delete executable.obseverState.aceAnchor;
+            if (executable.observerState.aceAnchor) {
+              executable.observerState.aceAnchor.dispose();
+              delete executable.observerState.aceAnchor;
             }
             return;
           }
 
           const statement = executable.parsedStatement;
-          if (!executable.obseverState.aceAnchor) {
-            executable.obseverState.aceAnchor = new AceAnchoredRange(this.editor);
+          if (!executable.observerState.aceAnchor) {
+            executable.observerState.aceAnchor = new AceAnchoredRange(this.editor);
           }
           const leadingEmptyLineCount = getLeadingEmptyLineCount(statement);
-          executable.obseverState.aceAnchor.move(statement.location, leadingEmptyLineCount);
-          const anchoredRange = executable.obseverState.aceAnchor;
+          executable.observerState.aceAnchor.move(statement.location, leadingEmptyLineCount);
+          const anchoredRange = executable.observerState.aceAnchor;
           anchoredRange.removeGutterCss(COMPLETED_CSS);
           anchoredRange.removeGutterCss(EXECUTING_CSS);
           anchoredRange.removeGutterCss(FAILED_CSS);