浏览代码

HUE-9028 [editor] Add support for select to execute multiple in notebook 2

When selecting multiple statements it will execute all of them in order without stopping as we now store the result per statement. It's also possible to execute multiple chains in parallel by selecting non-intersecting chains.

If a previous selected execution chain is intersecting a new execution it will cancel the previous chain.
Johan Ahlen 6 年之前
父节点
当前提交
8a93433e73

+ 6 - 4
desktop/core/src/desktop/js/apps/notebook2/components/ko.snippetExecuteActions.js → desktop/core/src/desktop/js/apps/notebook2/components/ko.executableActions.js

@@ -21,7 +21,7 @@ import I18n from 'utils/i18n';
 import { EXECUTABLE_UPDATED_EVENT, EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
 import DisposableComponent from 'ko/components/DisposableComponent';
 
-export const NAME = 'snippet-execute-actions';
+export const NAME = 'executable-actions';
 
 export const EXECUTE_ACTIVE_EXECUTABLE_EVENT = 'executable.active.executable';
 
@@ -57,7 +57,7 @@ const TEMPLATE = `
 </div>
 `;
 
-class SnippetExecuteActions extends DisposableComponent {
+class ExecutableActions extends DisposableComponent {
   constructor(params) {
     super();
     this.stopping = ko.observable(false);
@@ -96,7 +96,9 @@ class SnippetExecuteActions extends DisposableComponent {
   }
 
   async execute() {
-    this.beforeExecute();
+    if (this.beforeExecute) {
+      await this.beforeExecute();
+    }
     const executable = this.activeExecutable();
     if (executable) {
       await executable.reset();
@@ -112,4 +114,4 @@ class SnippetExecuteActions extends DisposableComponent {
   }
 }
 
-componentUtils.registerComponent(NAME, SnippetExecuteActions, TEMPLATE);
+componentUtils.registerComponent(NAME, ExecutableActions, TEMPLATE);

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

@@ -23,8 +23,6 @@ import componentUtils from 'ko/components/componentUtils';
 import { EXECUTABLE_UPDATED_EVENT, EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
 import DisposableComponent from 'ko/components/DisposableComponent';
 import { sleep } from 'utils/hueUtils';
-import { REDRAW_FIXED_HEADERS_EVENT } from 'apps/notebook2/events';
-import huePubSub from 'utils/huePubSub';
 
 export const NAME = 'executable-progress-bar';
 

+ 8 - 3
desktop/core/src/desktop/js/apps/notebook2/components/spec/ko.snippetExecuteActions.spec.js → desktop/core/src/desktop/js/apps/notebook2/components/spec/ko.executableActions.spec.js

@@ -1,9 +1,10 @@
 import huePubSub from 'utils/huePubSub';
 import { koSetup } from 'spec/jasmineSetup';
-import { NAME } from '../ko.snippetExecuteActions';
+import { NAME } from '../ko.executableActions';
 import { EXECUTABLE_UPDATED_EVENT, EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
+import { sleep } from 'utils/hueUtils';
 
-describe('ko.snippetExecuteActions.js', () => {
+describe('ko.executableActions.js', () => {
   const setup = koSetup();
 
   it('should render component', async () => {
@@ -11,6 +12,7 @@ describe('ko.snippetExecuteActions.js', () => {
       cancel: () => {},
       execute: () => {},
       isReady: () => true,
+      reset: () => {},
       nextExecutable: {},
       status: EXECUTION_STATUS.ready
     };
@@ -30,10 +32,11 @@ describe('ko.snippetExecuteActions.js', () => {
       cancel: () => {
         cancelCalled = true;
       },
-      execute: () => {
+      execute: async () => {
         executeCalled = true;
       },
       isReady: () => true,
+      reset: () => {},
       nextExecutable: {},
       status: EXECUTION_STATUS.ready
     };
@@ -49,6 +52,8 @@ describe('ko.snippetExecuteActions.js', () => {
     expect(wrapper.querySelector('[data-test="stop"]')).toBeFalsy();
     wrapper.querySelector('[data-test="execute"]').click();
 
+    await sleep(0);
+
     expect(executeCalled).toBeTruthy();
     mockExecutable.status = EXECUTION_STATUS.running;
     huePubSub.publish(EXECUTABLE_UPDATED_EVENT, mockExecutable);

+ 30 - 2
desktop/core/src/desktop/js/apps/notebook2/execution/executable.js

@@ -65,6 +65,7 @@ export default class Executable {
     this.executeStarted = 0;
     this.executeEnded = 0;
 
+    this.previousExecutable = undefined;
     this.nextExecutable = undefined;
   }
 
@@ -97,6 +98,24 @@ export default class Executable {
     );
   }
 
+  cancelBatchChain() {
+    if (this.previousExecutable) {
+      this.previousExecutable.nextExecutable = undefined;
+      this.previousExecutable.cancelBatchChain();
+      this.previousExecutable = undefined;
+    }
+
+    if (!this.isReady()) {
+      this.cancel();
+    }
+
+    if (this.nextExecutable) {
+      this.nextExecutable.previousExecutable = undefined;
+      this.nextExecutable.cancelBatchChain();
+      this.nextExecutable = undefined;
+    }
+  }
+
   async execute() {
     if (!this.isReady()) {
       return;
@@ -140,7 +159,7 @@ export default class Executable {
     statusCheckCount++;
 
     this.cancellables.push(
-      apiHelper.checkExecutionStatus({ executable: this }).done(queryStatus => {
+      apiHelper.checkExecutionStatus({ executable: this }).done(async queryStatus => {
         switch (this.status) {
           case EXECUTION_STATUS.success:
             this.executeEnded = Date.now();
@@ -155,6 +174,12 @@ export default class Executable {
               this.result = new ExecutionResult(this);
               this.result.fetchRows();
             }
+            if (this.nextExecutable) {
+              if (!this.nextExecutable.isReady()) {
+                await this.nextExecutable.reset();
+              }
+              this.nextExecutable.execute();
+            }
             break;
           case EXECUTION_STATUS.expired:
             this.executeEnded = Date.now();
@@ -214,11 +239,14 @@ export default class Executable {
         await this.close();
       } catch (err) {}
     }
+    this.handle = {
+      statement_id: 0
+    };
     this.setProgress(0);
+    this.setStatus(EXECUTION_STATUS.ready);
   }
 
   async close() {
-    console.log('closing');
     while (this.cancellables.length) {
       const nextCancellable = this.cancellables.pop();
       try {

+ 62 - 35
desktop/core/src/desktop/js/apps/notebook2/execution/executor.js

@@ -39,50 +39,77 @@ class Executor {
     this.executables = [];
   }
 
-  update(statementDetails) {
+  getExecutables(statementDetails) {
+    const allExecutablesIndex = {};
+    this.executables.forEach(executable => {
+      allExecutablesIndex[executable.getKey()] = executable;
+    });
+
+    const selectedExecutables = [];
+    let activeDatabase = this.database();
+    let currentSelectedIndex = 0;
     const newExecutables = statementDetails.precedingStatements
       .concat(statementDetails.activeStatement, statementDetails.followingStatements)
-      .map(
-        parsedStatement =>
-          new SqlExecutable({
-            parsedStatement: parsedStatement,
-            database: this.database(),
-            executor: this
-          })
-      );
+      .map(parsedStatement => {
+        if (/USE/i.test(parsedStatement.firstToken)) {
+          const dbMatch = parsedStatement.statement.match(/use\s+([^;]+)/i);
+          if (dbMatch) {
+            activeDatabase = dbMatch[1];
+          }
+        }
+        let executable = new SqlExecutable({
+          parsedStatement: parsedStatement,
+          database: activeDatabase,
+          executor: this
+        });
+        if (allExecutablesIndex[executable.getKey()]) {
+          executable = allExecutablesIndex[executable.getKey()];
+          delete allExecutablesIndex[executable.getKey()];
+        }
+        if (
+          currentSelectedIndex < statementDetails.selectedStatements.length &&
+          parsedStatement === statementDetails.selectedStatements[currentSelectedIndex]
+        ) {
+          selectedExecutables.push(executable);
+          currentSelectedIndex++;
+        }
+        return executable;
+      });
 
-    const existingExecutableIndex = {};
-    this.executables.forEach(executable => {
-      existingExecutableIndex[executable.getKey()] = executable;
-    });
+    const lostExecutables = Object.keys(allExecutablesIndex).map(key => allExecutablesIndex[key]);
+    return {
+      all: newExecutables,
+      lost: lostExecutables,
+      selected: selectedExecutables
+    };
+  }
 
-    let activeExecutable = new SqlExecutable({
-      parsedStatement: statementDetails.activeStatement,
-      database: this.database(),
-      executor: this
+  update(statementDetails, beforeExecute) {
+    const executables = this.getExecutables(statementDetails);
+
+    // Cancel any "lost" executables and any batch chain it's part of
+    executables.lost.forEach(lostExecutable => {
+      lostExecutable.cancelBatchChain();
     });
 
-    if (existingExecutableIndex[activeExecutable.getKey()]) {
-      activeExecutable = existingExecutableIndex[activeExecutable.getKey()];
-    }
+    // Cancel any intersecting batch chains and create a new chain if just before execute
+    if (beforeExecute) {
+      executables.selected.forEach(executable => executable.cancelBatchChain());
 
-    // Refresh the executables list
-    this.executables = newExecutables.map(newExecutable => {
-      let actualExecutable = newExecutable;
-      const existingExecutable = existingExecutableIndex[newExecutable.getKey()];
-      if (existingExecutable) {
-        actualExecutable = existingExecutable;
-        delete existingExecutableIndex[newExecutable.getKey()];
-      }
-      return actualExecutable;
-    });
+      let previous = undefined;
+      executables.selected.forEach(executable => {
+        if (previous) {
+          executable.previousExecutable = previous;
+          previous.nextExecutable = executable;
+        }
+        previous = executable;
+      });
+    }
 
-    // Cancel lost executables
-    Object.keys(existingExecutableIndex).forEach(key => {
-      existingExecutableIndex[key].cancel();
-    });
+    // Update the executables list
+    this.executables = executables.all;
 
-    return activeExecutable;
+    return executables.selected[0];
   }
 }
 

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

@@ -16,7 +16,6 @@
 
 import apiHelper from 'api/apiHelper';
 import Executable from 'apps/notebook2/execution/executable';
-import sqlStatementsParser from 'parse/sqlStatementsParser';
 
 const BATCHABLE_STATEMENT_TYPES = /ALTER|CREATE|DELETE|DROP|GRANT|INSERT|INVALIDATE|LOAD|SET|TRUNCATE|UPDATE|UPSERT|USE/i;
 
@@ -49,49 +48,6 @@ export default class SqlExecutable extends Executable {
     return this.database + '_' + this.parsedStatement.statement;
   }
 
-  /**
-   *
-   * @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;
-    sqlStatementsParser.parse(options.statement).forEach(parsedStatement => {
-      // If there's no first token it's a trailing comment
-      if (parsedStatement.firstToken) {
-        let skip = false;
-        // TODO: Do we want to send USE statements separately or do we want to send database as param instead?
-        if (/USE/i.test(parsedStatement.firstToken)) {
-          const dbMatch = parsedStatement.statement.match(/use\s+([^;]+)/i);
-          if (dbMatch) {
-            database = dbMatch[1];
-            skip = this.sourceType === 'impala' || this.sourceType === 'hive';
-          }
-        }
-        if (!skip) {
-          result.push(
-            new SqlExecutable({
-              executor: options.executor,
-              sourceType: options.sourceType,
-              compute: options.compute,
-              namespace: options.namespace,
-              database: database,
-              parsedStatement: parsedStatement
-            })
-          );
-        }
-      }
-    });
-    return result;
-  }
-
   canExecuteInBatch() {
     return this.parsedStatement && BATCHABLE_STATEMENT_TYPES.test(this.parsedStatement.firstToken);
   }

+ 10 - 9
desktop/core/src/desktop/js/apps/notebook2/snippet.js

@@ -19,10 +19,10 @@ import ko from 'knockout';
 import komapping from 'knockout.mapping';
 import { markdown } from 'markdown';
 
+import 'apps/notebook2/components/ko.executableActions';
 import 'apps/notebook2/components/ko.executableLogs';
 import 'apps/notebook2/components/ko.executableProgressBar';
 import 'apps/notebook2/components/ko.snippetEditorActions';
-import 'apps/notebook2/components/ko.snippetExecuteActions';
 import 'apps/notebook2/components/ko.snippetResults';
 
 import AceAutocompleteWrapper from 'apps/notebook/aceAutocompleteWrapper';
@@ -40,7 +40,7 @@ import {
   ACTIVE_STATEMENT_CHANGED_EVENT,
   REFRESH_STATEMENT_LOCATIONS_EVENT
 } from 'sql/aceLocationHandler';
-import { EXECUTE_ACTIVE_EXECUTABLE_EVENT } from 'apps/notebook2/components/ko.snippetExecuteActions';
+import { EXECUTE_ACTIVE_EXECUTABLE_EVENT } from 'apps/notebook2/components/ko.executableActions';
 
 // TODO: Remove. Temporary here for debug
 window.SqlExecutable = SqlExecutable;
@@ -369,6 +369,12 @@ export default class Snippet {
     this.lastExecutedStatement = ko.observable(null);
     this.statementsList = ko.observableArray();
 
+    let beforeExecute = false;
+    this.beforeExecute = () => {
+      beforeExecute = true;
+      huePubSub.publish(REFRESH_STATEMENT_LOCATIONS_EVENT, this);
+    };
+
     huePubSub.subscribe(
       ACTIVE_STATEMENT_CHANGED_EVENT,
       statementDetails => {
@@ -387,10 +393,9 @@ export default class Snippet {
               break;
             }
           }
-
           this.positionStatement(statementDetails.activeStatement);
-          this.activeExecutable(this.executor.update(statementDetails));
-
+          this.activeExecutable(this.executor.update(statementDetails, beforeExecute));
+          beforeExecute = false;
           if (statementDetails.activeStatement) {
             const _statements = [];
             statementDetails.precedingStatements.forEach(statement => {
@@ -1003,10 +1008,6 @@ export default class Snippet {
       isSqlEngine: this.isSqlDialect
     });
 
-    this.beforeExecute = () => {
-      huePubSub.publish(REFRESH_STATEMENT_LOCATIONS_EVENT, this);
-    };
-
     huePubSub.subscribe(EXECUTABLE_UPDATED_EVENT, executable => {
       if (this.activeExecutable() === executable) {
         this.status(executable.status);

+ 57 - 14
desktop/core/src/desktop/js/sql/aceLocationHandler.js

@@ -498,17 +498,30 @@ class AceLocationHandler {
       );
     };
 
+    const getFirstPosition = (editorPositionOne, editorPositionTwo) => {
+      if (editorPositionOne.row === editorPositionTwo.row) {
+        return editorPositionOne.column <= editorPositionTwo.column
+          ? editorPositionOne
+          : editorPositionTwo;
+      }
+      return editorPositionOne.row < editorPositionTwo.row ? editorPositionOne : editorPositionTwo;
+    };
+
+    const equalPositions = (editorPositionOne, editorPositionTwo) => {
+      return (
+        editorPositionOne.row === editorPositionTwo.row &&
+        editorPositionOne.column === editorPositionTwo.column
+      );
+    };
+
     let lastExecutingStatement = null;
     const updateActiveStatement = function(cursorChange) {
       if (!self.snippet.isSqlDialect()) {
         return;
       }
       const selectionRange = self.editor.getSelectionRange();
-      const editorLocation = selectionRange.start;
-      if (
-        selectionRange.start.row !== selectionRange.end.row ||
-        selectionRange.start.column !== selectionRange.end.column
-      ) {
+      const cursorLocation = selectionRange.start;
+      if (!equalPositions(selectionRange.start, selectionRange.end)) {
         if (!cursorChange && self.snippet.result && self.snippet.result.statement_range()) {
           let executingStatement = self.snippet.result.statement_range();
           // Row and col are 0 for both start and end on execute, so if the selection hasn't changed we'll use last known executed statement
@@ -522,10 +535,10 @@ class AceLocationHandler {
             executingStatement = lastExecutingStatement;
           }
           if (executingStatement.start.row === 0) {
-            editorLocation.column += executingStatement.start.column;
+            cursorLocation.column += executingStatement.start.column;
           } else if (executingStatement.start.row !== 0 || executingStatement.start.column !== 0) {
-            editorLocation.row += executingStatement.start.row;
-            editorLocation.column = executingStatement.start.column;
+            cursorLocation.row += executingStatement.start.row;
+            cursorLocation.column = executingStatement.start.column;
           }
           lastExecutingStatement = executingStatement;
         } else {
@@ -533,23 +546,45 @@ class AceLocationHandler {
         }
       }
 
-      if (cursorChange && activeStatement) {
+      if (cursorChange && activeStatement && !window.ENABLE_NOTEBOOK_2) {
         // Don't update when cursor stays in the same statement
-        if (isPointInside(activeStatement.location, editorLocation)) {
+        if (isPointInside(activeStatement.location, cursorLocation)) {
           return;
         }
       }
+      const selectedStatements = [];
       const precedingStatements = [];
       const followingStatements = [];
       activeStatement = null;
 
+      const firstSelectionPoint = getFirstPosition(selectionRange.start, selectionRange.end);
+      const lastSelectionPoint =
+        selectionRange.start === firstSelectionPoint ? selectionRange.end : selectionRange.start;
+
       let found = false;
       let statementIndex = 0;
+      let insideSelection = false;
       if (lastKnownStatements.length === 1) {
         activeStatement = lastKnownStatements[0];
       } else {
         lastKnownStatements.forEach(statement => {
-          if (isPointInside(statement.location, editorLocation)) {
+          if (!equalPositions(firstSelectionPoint, lastSelectionPoint)) {
+            if (!insideSelection && isPointInside(statement.location, firstSelectionPoint)) {
+              insideSelection = true;
+            }
+            if (insideSelection) {
+              selectedStatements.push(statement);
+
+              if (
+                isPointInside(statement.location, lastSelectionPoint) ||
+                (statement.location.last_line === lastSelectionPoint.row + 1 &&
+                  statement.location.last_column === lastSelectionPoint.column)
+              ) {
+                insideSelection = false;
+              }
+            }
+          }
+          if (isPointInside(statement.location, cursorLocation)) {
             statementIndex++;
             found = true;
             activeStatement = statement;
@@ -571,6 +606,10 @@ class AceLocationHandler {
         }
       }
 
+      if (!selectedStatements.length) {
+        selectedStatements.push(activeStatement);
+      }
+
       huePubSub.publish(ACTIVE_STATEMENT_CHANGED_EVENT, {
         id: self.editorId,
         editorChangeTime: lastKnownStatements.editorChangeTime,
@@ -578,11 +617,12 @@ class AceLocationHandler {
         totalStatementCount: lastKnownStatements.length,
         precedingStatements: precedingStatements,
         activeStatement: activeStatement,
+        selectedStatements: selectedStatements,
         followingStatements: followingStatements
       });
 
       if (activeStatement) {
-        self.checkForSyntaxErrors(activeStatement.location, editorLocation);
+        self.checkForSyntaxErrors(activeStatement.location, firstSelectionPoint);
       }
     };
 
@@ -609,6 +649,7 @@ class AceLocationHandler {
     let cursorChangePaused = false; // On change the cursor is also moved, this limits the calls while typing
 
     let lastStart;
+    let lastEnd;
     let lastCursorPosition;
     const changeSelectionListener = self.editor.on('changeSelection', () => {
       if (cursorChangePaused) {
@@ -628,13 +669,15 @@ class AceLocationHandler {
 
         // The active statement is initially the top one in the selection, batch execution updates this.
         const newStart = self.editor.getSelectionRange().start;
+        const newEnd = self.editor.getSelectionRange().end;
         if (
-          self.snippet.isSqlDialect() &&
-          (!lastStart || lastStart.row !== newStart.row || lastStart.column !== newStart.column)
+          (self.snippet.isSqlDialect() && (!lastStart || !equalPositions(lastStart, newStart))) ||
+          (window.ENABLE_NOTEBOOK_2 && (!lastEnd || !equalPositions(lastEnd, newEnd)))
         ) {
           window.clearTimeout(updateThrottle);
           updateActiveStatement(true);
           lastStart = newStart;
+          lastEnd = newEnd;
         }
       }, 100);
     });

+ 1 - 1
desktop/libs/notebook/src/notebook/templates/editor_components2.mako

@@ -1335,7 +1335,7 @@
 
   <script type ="text/html" id="snippet-execution-controls${ suffix }">
     <div class="snippet-actions clearfix">
-      <div class="pull-left" data-bind="component: { name: 'snippet-execute-actions', params: { activeExecutable: activeExecutable, beforeExecute: beforeExecute } }" />
+      <div class="pull-left" data-bind="component: { name: 'executable-actions', params: { activeExecutable: activeExecutable, beforeExecute: beforeExecute } }" />
       <!-- ko if: isSqlDialect() && !$root.isPresentationMode() -->
       <div class="pull-right" data-bind="component: { name: 'snippet-editor-actions', params: { snippet: $data } }" />
       <!-- /ko -->