Преглед изворни кода

HUE-9013 [editor] Switch to event based result handling for the result grid and chart components

This also introduces active and latest executable in the snippet to support multiple result handling
Johan Ahlen пре 6 година
родитељ
комит
2207d910bb

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

@@ -25,9 +25,24 @@ import 'apps/notebook2/components/resultChart/ko.resultChart';
 import 'apps/notebook2/components/resultGrid/ko.resultGrid';
 import { REDRAW_FIXED_HEADERS_EVENT } from 'apps/notebook2/components/resultGrid/ko.resultGrid';
 import { REDRAW_CHART_EVENT } from 'apps/notebook2/components/resultChart/ko.resultChart';
+import { EXECUTABLE_UPDATED_EVENT } from 'apps/notebook2/execution/executable';
+import { RESULT_TYPE, RESULT_UPDATED_EVENT } from 'apps/notebook2/execution/executionResult';
 
 export const NAME = 'snippet-results';
 
+const META_TYPE_TO_CSS = {
+  TINYINT_TYPE: 'sort-numeric',
+  SMALLINT_TYPE: 'sort-numeric',
+  INT_TYPE: 'sort-numeric',
+  BIGINT_TYPE: 'sort-numeric',
+  FLOAT_TYPE: 'sort-numeric',
+  DOUBLE_TYPE: 'sort-numeric',
+  DECIMAL_TYPE: 'sort-numeric',
+  TIMESTAMP_TYPE: 'sort-date',
+  DATE_TYPE: 'sort-date',
+  DATETIME_TYPE: 'sort-date'
+};
+
 const isNumericColumn = type =>
   ['tinyint', 'smallint', 'int', 'bigint', 'float', 'double', 'decimal', 'real'].indexOf(type) !==
   -1;
@@ -82,7 +97,7 @@ const TEMPLATE = `
           params: {
             data: data,
             editorMode: editorMode,
-            fetchResult: fetchResult,
+            fetchResult: fetchResult.bind($data),
             hasMore: hasMore,
             isPresentationMode: isPresentationMode,
             isResultFullScreenMode: isResultFullScreenMode,
@@ -117,23 +132,39 @@ class SnippetResults extends DisposableComponent {
     super();
     this.element = element;
 
-    this.type = params.type; // result
-    this.hasSomeResults = params.hasSomeResults; // result
-    this.images = params.images; // result
-    this.data = params.data; // result
-    this.meta = params.meta; // result
+    this.activeExecutable = params.activeExecutable;
+    this.latestExecutable = params.latestExecutable;
 
-    // Grid specific
     this.editorMode = params.editorMode;
-    this.fetchResult = params.fetchResult;
-    this.hasMore = params.hasMore;
     this.isPresentationMode = params.isPresentationMode;
     this.isResultFullScreenMode = params.isResultFullScreenMode;
     this.resultsKlass = params.resultsKlass;
-    this.status = params.status;
+    this.id = params.id; // TODO: Get rid of
+
+    huePubSub.subscribe(EXECUTABLE_UPDATED_EVENT, executable => {
+      if (this.activeExecutable() === executable) {
+        this.updateFromExecutable(executable);
+      }
+    });
+
+    const lastRenderedResult = undefined;
+    huePubSub.subscribe(RESULT_UPDATED_EVENT, executionResult => {
+      if (this.activeExecutable() === executionResult.executable) {
+        const refresh = lastRenderedResult === executionResult;
+        this.updateFromExecutionResult(executionResult, refresh);
+        this.lastRenderedResult = executionResult;
+      }
+    });
+
+    this.status = ko.observable();
+    this.type = ko.observable(RESULT_TYPE.TABLE);
+    this.meta = ko.observableArray();
+    this.data = ko.observableArray();
+    this.images = ko.observableArray();
+    this.hasMore = ko.observable();
+    this.hasResultSet = ko.observable();
 
-    // Chart specific
-    this.id = params.id;
+    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
@@ -173,6 +204,52 @@ class SnippetResults extends DisposableComponent {
       this.meta().filter(item => item.name !== '' && isNumericColumn(item.type))
     );
   }
+
+  reset() {
+    this.images([]);
+    this.data([]);
+    this.meta([]);
+    this.hasMore(false);
+    this.type(RESULT_TYPE.TABLE);
+    this.status(undefined);
+  }
+
+  updateFromExecutionResult(executionResult, refresh) {
+    if (refresh) {
+      this.reset();
+    }
+
+    if (executionResult) {
+      this.hasMore(executionResult.hasMore);
+      this.type(executionResult.type);
+
+      if (!this.meta().length && executionResult.meta.length) {
+        this.meta(
+          executionResult.meta.map((item, index) => ({
+            name: item.name,
+            type: item.type.replace(/_type/i, '').toLowerCase(),
+            comment: item.comment,
+            cssClass: META_TYPE_TO_CSS[item.type] || 'sort-string',
+            checked: ko.observable(true),
+            originalIndex: index
+          }))
+        );
+      }
+
+      if (executionResult.lastRows.length) {
+        this.data.push(...executionResult.lastRows);
+      }
+    }
+  }
+
+  updateFromExecutable(executable) {
+    this.hasResultSet(executable.handle.has_result_set);
+    if (!this.hasResultSet) {
+      this.reset();
+    }
+  }
+
+  fetchResult() {}
 }
 
 componentUtils.registerComponent(

+ 0 - 16
desktop/core/src/desktop/js/apps/notebook2/components/resultGrid/ko.resultGrid.js

@@ -187,28 +187,12 @@ class ResultGrid extends DisposableComponent {
 
     this.hueDatatable = undefined;
 
-    const adaptMeta = () => {
-      this.meta().forEach((item, index) => {
-        if (typeof item.checked === 'undefined') {
-          item.checked = ko.observable(true);
-          item.type = item.type.replace(/_type/i, '').toLowerCase();
-          item.originalIndex = index;
-        }
-      });
-    };
-
     this.trackKoSub(
       this.columnsVisible.subscribe(() => {
         defer(this.redrawFixedHeaders.bind(this));
       })
     );
 
-    this.trackKoSub(
-      this.meta.subscribe(() => {
-        adaptMeta();
-      })
-    );
-
     this.filteredMeta = ko.pureComputed(() => {
       if (!this.metaFilter() || this.metaFilter().query === '') {
         return this.meta();

+ 60 - 54
desktop/core/src/desktop/js/apps/notebook2/execution/executable.js

@@ -15,10 +15,11 @@
 // limitations under the License.
 
 import apiHelper from 'api/apiHelper';
-import { ExecutionResult } from 'apps/notebook2/execution/executionResult';
+import ExecutionResult from 'apps/notebook2/execution/executionResult';
 import hueAnalytics from 'utils/hueAnalytics';
 import huePubSub from 'utils/huePubSub';
 import sessionManager from 'apps/notebook2/execution/sessionManager';
+
 /**
  *
  * @type {{running: string, canceling: string, canceled: string, expired: string, waiting: string, success: string, ready: string, available: string, closed: string, starting: string}}
@@ -37,7 +38,7 @@ export const EXECUTION_STATUS = {
   closed: 'closed'
 };
 
-const EXECUTABLE_UPDATED_EVENT = 'hue.executable.updated';
+export const EXECUTABLE_UPDATED_EVENT = 'hue.executable.updated';
 
 export default class Executable {
   /**
@@ -48,18 +49,21 @@ export default class Executable {
    * @param {string} [options.statement] - Either supply a statement or a parsedStatement
    * @param {SqlStatementsParserResult} [options.parsedStatement] - Either supply a statement or a parsedStatement
    * @param {string} [options.database]
+   * @param {Executor} options.executor
    * @param {Session[]} [options.sessions]
    */
   constructor(options) {
     this.compute = options.compute;
     this.namespace = options.namespace;
     this.sourceType = options.sourceType;
+    this.executor = options.executor;
 
     this.handle = {
       statement_id: 0 // TODO: Get rid of need for initial handle in the backend
     };
     this.status = EXECUTION_STATUS.ready;
     this.progress = 0;
+    this.result = undefined;
 
     this.lastCancellable = undefined;
     this.notifyThrottle = -1;
@@ -87,55 +91,6 @@ export default class Executable {
       return;
     }
 
-    let statusCheckCount = 0;
-    let checkStatusTimeout = -1;
-
-    const checkStatus = () =>
-      new Promise((statusResolve, statusReject) => {
-        statusCheckCount++;
-        this.lastCancellable = apiHelper
-          .checkExecutionStatus({ executable: this })
-          .done(queryStatus => {
-            switch (this.status) {
-              case EXECUTION_STATUS.success:
-                this.setStatus(queryStatus);
-                this.setProgress(99); // TODO: why 99 here (from old code)?
-                statusResolve();
-                break;
-              case EXECUTION_STATUS.available:
-                this.setStatus(queryStatus);
-                this.setProgress(100);
-                statusResolve();
-                break;
-              case EXECUTION_STATUS.expired:
-                this.setStatus(queryStatus);
-                statusReject();
-                break;
-              case EXECUTION_STATUS.running:
-              case EXECUTION_STATUS.starting:
-              case EXECUTION_STATUS.waiting:
-                this.setStatus(queryStatus);
-                checkStatusTimeout = window.setTimeout(
-                  () => {
-                    checkStatus()
-                      .then(statusResolve)
-                      .catch(statusReject);
-                  },
-                  statusCheckCount > 45 ? 5000 : 1000
-                );
-                break;
-              default:
-                console.warn('Got unknown status ' + queryStatus);
-                statusReject();
-            }
-          })
-          .fail(statusReject);
-
-        this.lastCancellable.onCancel(() => {
-          window.clearTimeout(checkStatusTimeout);
-        });
-      });
-
     this.setStatus(EXECUTION_STATUS.running);
     this.setProgress(0);
 
@@ -143,14 +98,65 @@ export default class Executable {
       const session = await sessionManager.getSession({ type: this.sourceType });
       hueAnalytics.log('notebook', 'execute/' + this.sourceType);
       this.handle = await this.internalExecute(session);
-      await checkStatus();
-      this.result = new ExecutionResult(this);
+
+      if (this.handle.has_result_set && this.handle.sync) {
+        this.result = new ExecutionResult(this);
+        if (this.handle.sync) {
+          this.result.fetchRows();
+        }
+      }
+
+      this.checkStatus();
     } catch (err) {
       this.setStatus(EXECUTION_STATUS.failed);
       throw err;
     }
+  }
 
-    return this.result;
+  checkStatus(statusCheckCount) {
+    if (!statusCheckCount) {
+      statusCheckCount = 0;
+    }
+    statusCheckCount++;
+    let checkStatusTimeout = -1;
+    this.lastCancellable = apiHelper
+      .checkExecutionStatus({ executable: this })
+      .done(queryStatus => {
+        switch (this.status) {
+          case EXECUTION_STATUS.success:
+            this.setStatus(queryStatus);
+            this.setProgress(99); // TODO: why 99 here (from old code)?
+            break;
+          case EXECUTION_STATUS.available:
+            this.setStatus(queryStatus);
+            this.setProgress(100);
+            if (!this.result && this.handle.has_result_set) {
+              this.result = new ExecutionResult(this);
+              this.result.fetchRows();
+            }
+            break;
+          case EXECUTION_STATUS.expired:
+            this.setStatus(queryStatus);
+            break;
+          case EXECUTION_STATUS.running:
+          case EXECUTION_STATUS.starting:
+          case EXECUTION_STATUS.waiting:
+            this.setStatus(queryStatus);
+            checkStatusTimeout = window.setTimeout(
+              () => {
+                this.checkStatus(statusCheckCount);
+              },
+              statusCheckCount > 45 ? 5000 : 1000
+            );
+            break;
+          default:
+            console.warn('Got unknown status ' + queryStatus);
+        }
+      });
+
+    this.lastCancellable.onCancel(() => {
+      window.clearTimeout(checkStatusTimeout);
+    });
   }
 
   setLastCancellable(lastCancellable) {

+ 40 - 43
desktop/core/src/desktop/js/apps/notebook2/execution/executionResult.js

@@ -15,40 +15,37 @@
 // limitations under the License.
 
 import apiHelper from 'api/apiHelper';
+import huePubSub from 'utils/huePubSub';
 import { sleep } from 'utils/hueUtils';
+import { EXECUTION_STATUS } from './executable';
 
-/**
- *  available +----> fetching +----> done
- *      ^                     |
- *      |                     +----> fail
- *      |                     |
- *      +---------------------+
- *
- * If the handle indidcates that there's no result set available the ExecutionResult will have initial status set to
- * RESULT_STATUS.done
- *
- * @type { { canceling: string, canceled: string, fail: string, ready: string, executing: string, done: string } }
- */
-const RESULT_STATUS = {
-  ready: 'ready',
-  available: 'available',
-  fetching: 'fetching',
-  done: 'done',
-  fail: 'fail'
+export const RESULT_UPDATED_EVENT = 'hue.executable.result.updated';
+
+export const RESULT_TYPE = {
+  TABLE: 'table'
 };
 
-class ExecutionResult {
+export default class ExecutionResult {
   /**
    *
    * @param {Executable} executable
    */
   constructor(executable) {
     this.executable = executable;
-    this.status = executable.handle.has_result_set ? RESULT_STATUS.available : RESULT_STATUS.done;
+
+    this.type = RESULT_TYPE.TABLE;
+    this.state = {};
+    this.rows = [];
+    this.meta = [];
+    this.lastRows = [];
+    this.images = [];
+    this.type = undefined;
+    this.hasMore = true;
+    this.isEscaped = false;
   }
 
   async fetchResultSize() {
-    if (this.status === RESULT_STATUS.fail) {
+    if (this.executable.status === EXECUTION_STATUS.failed) {
       return;
     }
 
@@ -78,38 +75,38 @@ class ExecutionResult {
   /**
    * Fetches additional rows
    *
-   * @param {Object} options
-   * @param {number} options.rows
+   * @param {Object} [options]
+   * @param {number} [options.rows]
    * @param {boolean} [options.startOver]
    *
    * @return {Promise}
    */
   async fetchRows(options) {
-    if (this.status !== RESULT_STATUS.available) {
+    if (this.executable.status !== EXECUTION_STATUS.available) {
       return Promise.reject();
     }
 
-    this.status = RESULT_STATUS.fetching;
+    const resultResponse = await apiHelper.fetchResults({
+      executable: this.executable,
+      rows: (options && options.rows) || 100,
+      startOver: options && options.startOver
+    });
 
-    try {
-      const resultResponse = await apiHelper.fetchResults({
-        executable: this.executable,
-        rows: options.rows,
-        startOver: !!options.startOver
-      });
+    const initialIndex = this.rows.length;
+    resultResponse.data.forEach((row, index) => {
+      row.unshift(initialIndex + index + 1);
+    });
 
-      if (resultResponse.has_more) {
-        this.status = RESULT_STATUS.available;
-      } else {
-        this.status = RESULT_STATUS.done;
-      }
-
-      return resultResponse;
-    } catch (err) {
-      this.status = RESULT_STATUS.fail;
-      return Promise.reject(err);
+    this.rows.push(...resultResponse.data);
+    this.lastRows = resultResponse.data;
+    if (!this.meta.length) {
+      this.meta = resultResponse.meta;
+      this.meta.unshift({ type: 'INT_TYPE', name: '', comment: null });
     }
+    this.hasMore = resultResponse.has_more;
+    this.isEscaped = resultResponse.isEscaped;
+    this.type = resultResponse.type;
+
+    huePubSub.publish(RESULT_UPDATED_EVENT, this);
   }
 }
-
-export { RESULT_STATUS, ExecutionResult };

+ 16 - 28
desktop/core/src/desktop/js/apps/notebook2/execution/executor.js

@@ -16,6 +16,7 @@
 
 import SqlExecutable from 'apps/notebook2/execution/sqlExecutable';
 import { EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
+import { EXECUTABLE_UPDATED_EVENT } from 'apps/notebook2/execution/executable';
 import huePubSub from 'utils/huePubSub';
 import sessionManager from 'apps/notebook2/execution/sessionManager';
 
@@ -56,12 +57,12 @@ class Executor {
     this.executed = [];
 
     if (this.isSqlEngine) {
-      this.toExecute = SqlExecutable.fromStatement(options);
+      this.toExecute = SqlExecutable.fromStatement({ executor: this, ...options });
     } else {
       throw new Error('Not implemented yet');
     }
 
-    huePubSub.subscribe('hue.executable.updated', executable => {
+    huePubSub.subscribe(EXECUTABLE_UPDATED_EVENT, executable => {
       if (
         executable === this.currentExecutable ||
         this.executed.some(executed => executed === executable)
@@ -85,32 +86,19 @@ class Executor {
   }
 
   async executeNext() {
-    return new Promise((resolve, reject) => {
-      const executeBatch = () => {
-        if (this.toExecute.length === 0) {
-          reject();
-        } else {
-          this.currentExecutable = this.toExecute.shift();
-          this.currentExecutable
-            .execute()
-            .then(executionResult => {
-              this.executed.push(this.currentExecutable);
-              this.currentExecutable = undefined;
-
-              if (this.canExecuteNextInBatch()) {
-                this.executeNext()
-                  .then(executeBatch)
-                  .catch(reject);
-              } else {
-                resolve(executionResult);
-              }
-            })
-            .catch(reject);
-        }
-      };
-
-      executeBatch();
-    });
+    if (this.toExecute.length === 0) {
+      return;
+    }
+
+    this.currentExecutable = this.toExecute.shift();
+
+    await this.currentExecutable.execute();
+
+    this.executed.push(this.currentExecutable);
+
+    if (this.canExecuteNextInBatch()) {
+      await this.executeNext();
+    }
   }
 
   canExecuteNextInBatch() {

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

@@ -29,6 +29,7 @@ export default class SqlExecutable extends Executable {
    * @param {string} [options.statement] - Either supply a statement or a parsedStatement
    * @param {SqlStatementsParserResult} [options.parsedStatement] - Either supply a statement or a parsedStatement
    * @param {string} [options.database]
+   * @param {Executor} options.executor
    * @param {Session[]} [options.sessions]
    */
   constructor(options) {
@@ -56,6 +57,17 @@ export default class SqlExecutable extends Executable {
     });
   }
 
+  /**
+   *
+   * @param options
+   * @param options.database
+   * @param options.statement
+   * @param options.executor
+   * @param options.sourceType
+   * @param options.compute
+   * @param options.namespace
+   * @return {Array}
+   */
   static fromStatement(options) {
     const result = [];
     let database = options.database;
@@ -74,6 +86,7 @@ export default class SqlExecutable extends Executable {
         if (!skip) {
           result.push(
             new SqlExecutable({
+              executor: options.executor,
               sourceType: options.sourceType,
               compute: options.compute,
               namespace: options.namespace,

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

@@ -27,7 +27,7 @@ import 'apps/notebook2/components/ko.snippetResults';
 import AceAutocompleteWrapper from 'apps/notebook/aceAutocompleteWrapper';
 import apiHelper from 'api/apiHelper';
 import dataCatalog from 'catalog/dataCatalog';
-import Executor from 'apps/notebook2/execution/executor';
+import Executor, { EXECUTOR_UPDATED_EVENT } from 'apps/notebook2/execution/executor';
 import hueAnalytics from 'utils/hueAnalytics';
 import huePubSub from 'utils/huePubSub';
 import hueUtils from 'utils/hueUtils';
@@ -1008,10 +1008,15 @@ export default class Snippet {
       timeout: this.parentVm.autocompleteTimeout
     });
 
-    huePubSub.subscribe('hue.executor.updated', details => {
+    this.latestExecutable = ko.observable();
+    this.activeExecutable = ko.observable();
+
+    huePubSub.subscribe(EXECUTOR_UPDATED_EVENT, details => {
       const executable = details.executable;
 
       if (details.executor === this.executor()) {
+        this.latestExecutable(executable);
+        this.activeExecutable(executable); // TODO: Move to pureComputed (based on cursor)
         this.status(executable.status);
         this.progress(executable.progress);
       }

+ 6 - 18
desktop/libs/notebook/src/notebook/templates/editor_components2.mako

@@ -735,19 +735,13 @@
           <div class="tab-pane" id="queryResults" data-bind="css: {'active': currentQueryTab() == 'queryResults'}">
             <!-- ko if: ['text', 'jar', 'py', 'markdown'].indexOf(type()) === -1 -->
               <!-- ko component: { name: 'snippet-results', params: {
-                data: result.data,
+                latestExecutable: latestExecutable,
+                activeExecutable: activeExecutable,
                 editorMode: parentVm.editorMode,
-                fetchResult: result.fetchMoreRows,
-                hasMore: result.hasMore,
-                hasSomeResults: result.hasSomeResults,
                 id: id,
-                images: result.images,
                 isPresentationMode: parentNotebook.isPresentationMode,
                 isResultFullScreenMode: parentVm.isResultFullScreenMode,
-                meta: result.meta,
-                resultsKlass: resultsKlass,
-                status: status,
-                type: result.type,
+                resultsKlass: resultsKlass
               }} --><!-- /ko -->
             <!-- /ko -->
           </div>
@@ -889,19 +883,13 @@
             <!-- /ko -->
             <!-- ko if: !$root.editorMode() && ['text', 'jar', 'java', 'distcp', 'shell', 'mapreduce', 'py', 'markdown'].indexOf(type()) === -1 -->
               <!-- ko component: { name: 'snippet-results', params: {
-                data: result.data,
+                latestExecutable: latestExecutable,
+                activeExecutable: activeExecutable,
                 editorMode: parentVm.editorMode,
-                fetchResult: result.fetchMoreRows,
-                hasMore: result.hasMore,
-                hasSomeResults: result.hasSomeResults,
                 id: id,
-                images: result.images,
                 isPresentationMode: parentNotebook.isPresentationMode,
                 isResultFullScreenMode: parentVm.isResultFullScreenMode,
-                meta: result.meta,
-                resultsKlass: resultsKlass,
-                status: status,
-                type: result.type,
+                resultsKlass: resultsKlass
               }} --><!-- /ko -->
             <!-- /ko -->
             <div class="clearfix"></div>