Преглед на файлове

HUE-9028 [editor] Switch to fixed executor per snippet

Johan Ahlen преди 6 години
родител
ревизия
89d0279cc6

+ 0 - 10
desktop/core/src/desktop/js/apps/notebook2/app.js

@@ -876,16 +876,6 @@ export const initNotebook2 = () => {
         }, 50);
       });
 
-      $(document).on('executeStarted', (e, options) => {
-        const $snip = $('#snippet_' + options.snippet.id());
-        $snip.find('.progress-snippet').animate(
-          {
-            height: '3px'
-          },
-          100
-        );
-      });
-
       $(document).on('renderDataError', (e, options) => {
         huePubSub.publish(SHOW_NORMAL_RESULT_EVENT);
       });

+ 1 - 1
desktop/core/src/desktop/js/apps/notebook2/components/ko.executableProgressBar.js

@@ -66,7 +66,7 @@ class ExecutableProgressBar extends DisposableComponent {
     });
 
     const updateSub = huePubSub.subscribe(EXECUTOR_UPDATED_EVENT, executorUpdate => {
-      if (this.snippet.executor() === executorUpdate.executor) {
+      if (this.snippet.executor === executorUpdate.executor) {
         this.status(executorUpdate.executable.status);
         this.progress(executorUpdate.executable.progress);
       }

+ 26 - 15
desktop/core/src/desktop/js/apps/notebook2/components/ko.snippetExecuteActions.js

@@ -21,6 +21,7 @@ import huePubSub from 'utils/huePubSub';
 import I18n from 'utils/i18n';
 import { EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
 import { EXECUTOR_UPDATED_EVENT } from 'apps/notebook2/execution/executor';
+import DisposableComponent from 'ko/components/DisposableComponent';
 
 export const NAME = 'snippet-execute-actions';
 
@@ -28,10 +29,17 @@ const TEMPLATE = `
 <div class="snippet-execute-actions" data-test="${NAME}">
   <div class="btn-group">
     <!-- ko if: status() !== '${EXECUTION_STATUS.running}' -->
+    <!-- ko ifnot: hasMoreToExecute -->
     <button class="btn btn-primary btn-mini btn-execute disable-feedback" data-test="execute" data-bind="click: execute"><i class="fa fa-play fa-fw"></i> ${I18n(
       'Execute'
     )}</button>
     <!-- /ko -->
+    <!-- ko if: hasMoreToExecute -->
+    <button class="btn btn-primary btn-mini btn-execute disable-feedback" data-test="execute" data-bind="click: executeNext"><i class="fa fa-play fa-fw"></i> ${I18n(
+      'Continue'
+    )}</button>
+    <!-- /ko -->
+    <!-- /ko -->
     <!-- ko if: status() === '${EXECUTION_STATUS.running}' -->
     <!-- ko ifnot: stopping -->
     <button class="btn btn-primary btn-mini btn-execute disable-feedback" data-test="stop" data-bind="click: stop"><i class="fa fa-stop fa-fw"></i> ${I18n(
@@ -48,37 +56,40 @@ const TEMPLATE = `
 </div>
 `;
 
-class SnippetExecuteActions {
+class SnippetExecuteActions extends DisposableComponent {
   constructor(params) {
-    this.disposals = [];
+    super();
     this.stopping = ko.observable(false);
     this.snippet = params.snippet;
     this.status = ko.observable(EXECUTION_STATUS.ready);
-    const updateSub = huePubSub.subscribe(EXECUTOR_UPDATED_EVENT, executorUpdate => {
-      if (this.snippet.executor() === executorUpdate.executor) {
-        this.status(executorUpdate.executable.status);
-      }
-    });
-    this.disposals.push(() => {
-      updateSub.remove();
-    });
+    this.hasMoreToExecute = ko.observable(false);
+    this.trackPubSub(
+      huePubSub.subscribe(EXECUTOR_UPDATED_EVENT, executorUpdate => {
+        if (this.snippet.executor === executorUpdate.executor) {
+          this.hasMoreToExecute(!!executorUpdate.executor.hasMoreToExecute());
+          this.status(executorUpdate.executable.status);
+        }
+      })
+    );
   }
 
   async stop() {
     if (this.stopping()) {
       return;
     }
-    if (this.snippet.executor()) {
-      this.stopping(true);
-      await this.snippet.executor().cancel();
-      this.stopping(false);
-    }
+    this.stopping(true);
+    await this.snippet.executor.cancel();
+    this.stopping(false);
   }
 
   execute() {
     this.snippet.execute();
   }
 
+  executeNext() {
+    this.snippet.executeNext();
+  }
+
   dispose() {
     while (this.disposals.length) {
       this.disposals.pop()();

+ 35 - 4
desktop/core/src/desktop/js/apps/notebook2/components/ko.snippetResults.js

@@ -62,6 +62,10 @@ const TEMPLATE = `
       <button class="btn btn-editor btn-mini disable-feedback" data-bind="toggle: showGrid, css: { 'active': showGrid }"><i class="fa fa-fw fa-th"></i> ${ I18n('Grid') }</button>
       <button class="btn btn-editor btn-mini disable-feedback" data-bind="toggle: showChart, css: { 'active': showChart }"><i class="hcha fa-fw hcha-bar-chart"></i> ${ I18n('Chart') }</button>
     </div>
+    <div class="btn-group pull-right">
+      <button class="btn btn-editor btn-mini disable-feedback" data-bind="toggle: showLatestResult, css: { 'active': showLatestResult }">${ I18n('Latest') }</button>
+      <button class="btn btn-editor btn-mini disable-feedback" data-bind="toggle: showLatestResult, css: { 'active': !showLatestResult() }">${ I18n('Active') }</button>
+    </div>
     <div class="btn-group pull-right">
       <button class="btn btn-editor btn-mini disable-feedback" data-bind="toggle: isResultFullScreenMode">
         <!-- ko if: isResultFullScreenMode -->
@@ -91,7 +95,7 @@ const TEMPLATE = `
       <!-- /ko -->
     </div>
     <div class="table-results" data-bind="visible: type() === 'table'" style="display: none;">
-      <div data-bind="visible: showGrid" style="display: none; position: relative;">
+      <div data-bind="visible: showGrid() && data().length" style="display: none; position: relative;">
         <!-- ko component: { 
           name: 'result-grid',
           params: {
@@ -108,7 +112,7 @@ const TEMPLATE = `
           }
         } --><!-- /ko -->
       </div>
-      <div data-bind="visible: showChart" style="display: none; position: relative;">
+      <div data-bind="visible: showChart() && data().length" style="display: none; position: relative;">
         <!-- ko component: {
           name: 'result-chart',
           params: {
@@ -123,6 +127,14 @@ const TEMPLATE = `
           }
         } --><!-- /ko -->
       </div>
+      <div data-bind="visible: !data().length" style="display: none;">
+        <!-- ko if: showLatestResult -->
+        <h1 class="empty">${ I18n('Execute a query to see the latest result.') }</h1>
+        <!-- /ko -->
+        <!-- ko ifnot: showLatestResult -->
+        <h1 class="empty">${ I18n('Execute the active statement to see the result.') }</h1>
+        <!-- /ko -->
+      </div>
     </div>
   </div>
 </div>
@@ -136,6 +148,11 @@ class SnippetResults extends DisposableComponent {
     this.activeExecutable = params.activeExecutable;
     this.latestExecutable = params.latestExecutable;
 
+    this.showLatestResult = ko.observable(true);
+    this.selectedExecutable = ko.pureComputed(() =>
+      this.showLatestResult() ? this.latestExecutable() : this.activeExecutable()
+    );
+
     this.editorMode = params.editorMode;
     this.isPresentationMode = params.isPresentationMode;
     this.isResultFullScreenMode = params.isResultFullScreenMode;
@@ -192,23 +209,37 @@ class SnippetResults extends DisposableComponent {
     );
 
     huePubSub.subscribe(EXECUTABLE_UPDATED_EVENT, executable => {
-      if (this.activeExecutable() === executable) {
+      if (this.selectedExecutable() === executable) {
         this.updateFromExecutable(executable);
       }
     });
 
     let lastRenderedResult = undefined;
     huePubSub.subscribe(RESULT_UPDATED_EVENT, executionResult => {
-      if (this.activeExecutable() === executionResult.executable) {
+      if (this.selectedExecutable() === executionResult.executable) {
         const refresh = lastRenderedResult !== executionResult;
         this.updateFromExecutionResult(executionResult, refresh);
         lastRenderedResult = executionResult;
       }
     });
+
+    this.trackKoSub(
+      this.selectedExecutable.subscribe(executable => {
+        if (executable && executable.result) {
+          if (executable !== lastRenderedResult) {
+            this.updateFromExecutionResult(executable.result, true);
+            lastRenderedResult = executable;
+          }
+        } else {
+          this.reset();
+        }
+      })
+    );
   }
 
   reset() {
     this.images([]);
+    this.lastFetchedRows([]);
     this.data([]);
     this.meta([]);
     this.hasMore(false);

+ 46 - 8
desktop/core/src/desktop/js/apps/notebook2/execution/executor.js

@@ -31,6 +31,13 @@ const EXECUTION_FLOW = {
 
 export const EXECUTOR_UPDATED_EVENT = 'hue.executor.updated';
 
+const parsedStatementEquals = (a, b) =>
+  a.statement === b.statement &&
+  a.location.first_column === b.location.first_column &&
+  a.location.last_column === b.location.last_column &&
+  a.location.first_line === b.location.first_line &&
+  a.location.last_line === b.location.last_line;
+
 class Executor {
   /**
    * @param options
@@ -48,6 +55,7 @@ class Executor {
     this.namespace = options.namespace;
     this.database = options.database;
     this.isSqlEngine = options.isSqlEngine;
+    this.statement = options.statement;
     this.executionFlow = this.isSqlEngine
       ? options.executionFlow || EXECUTION_FLOW.batch
       : EXECUTION_FLOW.step;
@@ -56,12 +64,6 @@ class Executor {
     this.currentExecutable = undefined;
     this.executed = [];
 
-    if (this.isSqlEngine) {
-      this.toExecute = SqlExecutable.fromStatement({ executor: this, ...options });
-    } else {
-      throw new Error('Not implemented yet');
-    }
-
     huePubSub.subscribe(EXECUTABLE_UPDATED_EVENT, executable => {
       if (
         executable === this.currentExecutable ||
@@ -75,6 +77,38 @@ class Executor {
     });
   }
 
+  getExecutable(parsedStatement) {
+    return this.executed
+      .concat(this.currentExecutable, this.toExecute)
+      .find(
+        executable =>
+          executable &&
+          executable.parsedStatement &&
+          parsedStatementEquals(parsedStatement, executable.parsedStatement)
+      );
+  }
+
+  async reset() {
+    if (this.isRunning()) {
+      await this.cancel();
+    }
+
+    if (this.isSqlEngine()) {
+      this.toExecute = SqlExecutable.fromStatement({
+        executor: this,
+        statement: this.statement(),
+        database: this.database(),
+        sourceType: this.sourceType(),
+        compute: this.compute(),
+        namespace: this.namespace()
+      });
+      this.executed = [];
+      this.currentExecutable = undefined;
+    } else {
+      throw new Error('Not implemented yet');
+    }
+  }
+
   isRunning() {
     return this.currentExecutable && this.currentExecutable.status === EXECUTION_STATUS.running;
   }
@@ -85,8 +119,12 @@ class Executor {
     }
   }
 
+  hasMoreToExecute() {
+    return !!this.toExecute.length;
+  }
+
   async executeNext() {
-    if (this.toExecute.length === 0) {
+    if (!this.toExecute.length) {
       return;
     }
 
@@ -104,7 +142,7 @@ class Executor {
   canExecuteNextInBatch() {
     return (
       !this.executed.length ||
-      (this.isSqlEngine &&
+      (this.isSqlEngine() &&
         this.executionFlow !== EXECUTION_FLOW.step &&
         this.currentExecutable &&
         this.currentExecutable.canExecuteInBatch())

+ 27 - 21
desktop/core/src/desktop/js/apps/notebook2/snippet.js

@@ -209,8 +209,6 @@ export default class Snippet {
 
     this.errors = ko.observableArray([]);
 
-    this.executor = ko.observable();
-
     this.aceErrorsHolder = ko.observableArray([]);
     this.aceWarningsHolder = ko.observableArray([]);
 
@@ -386,8 +384,10 @@ export default class Snippet {
           }
           if (statementDetails.activeStatement) {
             this.positionStatement(statementDetails.activeStatement);
+            this.activeExecutable(this.executor.getExecutable(statementDetails.activeStatement));
           } else {
             this.positionStatement(null);
+            this.activeExecutable(undefined);
           }
 
           if (statementDetails.activeStatement) {
@@ -1011,10 +1011,19 @@ export default class Snippet {
     this.latestExecutable = ko.observable();
     this.activeExecutable = ko.observable();
 
+    this.executor = new Executor({
+      compute: this.compute,
+      database: this.database,
+      sourceType: this.type,
+      namespace: this.namespace,
+      statement: this.statement,
+      isSqlEngine: this.isSqlDialect
+    });
+
     huePubSub.subscribe(EXECUTOR_UPDATED_EVENT, details => {
       const executable = details.executable;
 
-      if (details.executor === this.executor()) {
+      if (details.executor === this.executor) {
         this.latestExecutable(executable);
         this.activeExecutable(executable); // TODO: Move to pureComputed (based on cursor)
         this.status(executable.status);
@@ -1219,7 +1228,7 @@ export default class Snippet {
     });
   }
 
-  async execute(automaticallyTriggered) {
+  async executeNext() {
     hueAnalytics.log('notebook', 'execute/' + this.type());
 
     const now = new Date().getTime();
@@ -1245,7 +1254,14 @@ export default class Snippet {
       this.lastAceSelectionRowOffset(Math.min(selectionRange.start.row, selectionRange.end.row));
     }
 
-    $(document).trigger('executeStarted', { vm: this.parentVm, snippet: this });
+    const $snip = $('#snippet_' + this.id());
+    $snip.find('.progress-snippet').animate(
+      {
+        height: '3px'
+      },
+      100
+    );
+
     $('.jHueNotify').remove();
     this.parentNotebook.forceHistoryInitialHeight(true);
     this.errors([]);
@@ -1258,23 +1274,8 @@ export default class Snippet {
 
     this.startLongOperationTimeout();
 
-    if (this.executor() && this.executor().isRunning()) {
-      this.executor().cancel();
-    }
-
-    this.executor(
-      new Executor({
-        compute: this.compute(),
-        database: this.database(),
-        sourceType: this.type(),
-        namespace: this.namespace(),
-        statement: this.statement(),
-        isSqlEngine: this.isSqlDialect()
-      })
-    );
-
     try {
-      const result = await this.executor().executeNext();
+      const result = await this.executor.executeNext();
 
       this.stopLongOperationTimeout();
       this.result.clear();
@@ -1289,6 +1290,11 @@ export default class Snippet {
     }
   }
 
+  async execute() {
+    await this.executor.reset();
+    await this.executeNext();
+  }
+
   async exportHistory() {
     const historyResponse = await apiHelper.getHistory({ type: this.type(), limit: 500 });