Переглянути джерело

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>