Explorar o código

HUE-9028 [editor] Enable parallel click to execute in notebook 2

Johan Ahlen %!s(int64=6) %!d(string=hai) anos
pai
achega
3408ff507a

+ 5 - 3
desktop/core/src/desktop/js/api/apiHelper.js

@@ -2018,7 +2018,7 @@ class ApiHelper {
   static adaptExecutableToNotebook(executable, session) {
     const statement = executable.getStatement();
     const snippet = {
-      type: executable.sourceType,
+      type: executable.executor.sourceType(),
       result: {
         handle: executable.handle
       },
@@ -2027,15 +2027,17 @@ class ApiHelper {
       statement_raw: statement,
       statement: statement,
       variables: [],
-      compute: executable.compute,
+      compute: executable.executor.compute(),
+      namespace: executable.executor.namespace(),
       database: executable.database,
       properties: { settings: [] }
     };
 
     const notebook = {
-      type: executable.sourceType,
+      type: executable.executor.sourceType(),
       snippets: [snippet],
       id: executable.notebookId,
+      uuid: hueUtils.UUID(),
       name: '',
       isSaved: false,
       sessions: session ? [session] : []

+ 28 - 10
desktop/core/src/desktop/js/apps/notebook2/components/ko.executableLogs.js

@@ -123,29 +123,47 @@ class ExecutableLogs extends DisposableComponent {
 
     this.subscribe(EXECUTABLE_UPDATED_EVENT, executable => {
       if (this.activeExecutable() === executable) {
-        this.hasResultset(executable.handle.has_result_set);
-        this.status(executable.status);
-        this.sourceType(executable.sourceType);
-        if (!this.compute) {
-          this.compute = executable.compute;
-        }
+        this.updateFromExecutable(executable);
       }
     });
 
+    this.subscribe(this.activeExecutable, this.updateFromExecutable.bind(this));
+
     this.subscribe(RESULT_UPDATED_EVENT, executionResult => {
       if (this.activeExecutable() === executionResult.executable) {
-        this.fetchedOnce(executionResult.fetchedOnce);
-        this.hasEmptyResult(executionResult.rows.length === 0);
+        this.updateFromResult(executionResult);
       }
     });
 
     this.subscribe(LOGS_UPDATED_EVENT, executionLogs => {
       if (this.activeExecutable() === executionLogs.executable) {
-        this.logs(executionLogs.fullLog);
-        this.jobs(executionLogs.jobs);
+        this.updateFromLogs(executionLogs);
       }
     });
   }
+
+  updateFromExecutable(executable) {
+    this.hasResultset(executable.handle.has_result_set);
+    this.status(executable.status);
+    this.sourceType(executable.sourceType);
+    if (!this.compute) {
+      this.compute = executable.compute;
+    }
+    if (executable.result) {
+      this.updateFromResult(executable.result);
+    }
+    this.updateFromLogs(executable.logs);
+  }
+
+  updateFromLogs(executionLogs) {
+    this.logs(executionLogs.fullLog);
+    this.jobs(executionLogs.jobs);
+  }
+
+  updateFromResult(executionResult) {
+    this.fetchedOnce(executionResult.fetchedOnce);
+    this.hasEmptyResult(executionResult.rows.length === 0);
+  }
 }
 
 componentUtils.registerComponent(NAME, ExecutableLogs, TEMPLATE);

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

@@ -19,9 +19,7 @@ import ko from 'knockout';
 import 'ko/bindings/ko.publish';
 
 import componentUtils from 'ko/components/componentUtils';
-import { EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
-import huePubSub from 'utils/huePubSub';
-import { EXECUTOR_UPDATED_EVENT } from 'apps/notebook2/execution/executor';
+import { EXECUTABLE_UPDATED_EVENT, EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
 import DisposableComponent from 'ko/components/DisposableComponent';
 
 export const NAME = 'executable-progress-bar';
@@ -35,7 +33,7 @@ const TEMPLATE = `
 class ExecutableProgressBar extends DisposableComponent {
   constructor(params) {
     super();
-    this.snippet = params.snippet;
+    this.activeExecutable = params.activeExecutable;
 
     this.status = ko.observable();
     this.progress = ko.observable(0);
@@ -65,15 +63,16 @@ class ExecutableProgressBar extends DisposableComponent {
       return Math.max(2, this.progress()) + '%';
     });
 
-    const updateSub = huePubSub.subscribe(EXECUTOR_UPDATED_EVENT, executorUpdate => {
-      if (this.snippet.executor === executorUpdate.executor) {
-        this.status(executorUpdate.executable.status);
-        this.progress(executorUpdate.executable.progress);
+    this.subscribe(EXECUTABLE_UPDATED_EVENT, executable => {
+      if (this.activeExecutable() === executable) {
+        this.status(executable.status);
+        this.progress(executable.progress);
       }
     });
 
-    this.addDisposalCallback(() => {
-      updateSub.remove();
+    this.subscribe(this.activeExecutable, executable => {
+      this.status(executable.status);
+      this.progress(executable.progress);
     });
   }
 }

+ 33 - 16
desktop/core/src/desktop/js/apps/notebook2/components/ko.snippetExecuteActions.js

@@ -18,12 +18,13 @@ import ko from 'knockout';
 
 import componentUtils from 'ko/components/componentUtils';
 import I18n from 'utils/i18n';
-import { EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
-import { EXECUTOR_UPDATED_EVENT } from 'apps/notebook2/execution/executor';
+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 EXECUTE_ACTIVE_EXECUTABLE_EVENT = 'executable.active.executable';
+
 // prettier-ignore
 const TEMPLATE = `
 <div class="snippet-execute-actions" data-test="${NAME}">
@@ -60,38 +61,54 @@ class SnippetExecuteActions extends DisposableComponent {
   constructor(params) {
     super();
     this.stopping = ko.observable(false);
-    this.snippet = params.snippet;
+    this.activeExecutable = params.activeExecutable;
     this.status = ko.observable(EXECUTION_STATUS.ready);
     this.hasMoreToExecute = ko.observable(false);
-    this.subscribe(EXECUTOR_UPDATED_EVENT, executorUpdate => {
-      if (this.snippet.executor === executorUpdate.executor) {
-        this.hasMoreToExecute(!!executorUpdate.executor.hasMoreToExecute());
-        this.status(executorUpdate.executable.status);
+
+    this.subscribe(EXECUTABLE_UPDATED_EVENT, executable => {
+      if (this.activeExecutable() === executable) {
+        this.hasMoreToExecute(!!executable.nextExecutable);
+        this.status(executable.status);
+      }
+    });
+
+    this.subscribe(EXECUTE_ACTIVE_EXECUTABLE_EVENT, executable => {
+      if (this.activeExecutable() === executable) {
+        this.execute();
       }
     });
+
+    this.subscribe(this.activeExecutable, executable => {
+      // TODO: If previousExecutable show 'waiting...' ?
+      this.hasMoreToExecute(!!executable.nextExecutable);
+      this.status(executable.status);
+    });
   }
 
   async stop() {
-    if (this.stopping()) {
+    if (this.stopping() || !this.activeExecutable()) {
       return;
     }
     this.stopping(true);
-    await this.snippet.executor.cancel();
+    await this.activeExecutable().cancel();
     this.stopping(false);
   }
 
   execute() {
-    this.snippet.execute();
+    const executable = this.activeExecutable();
+    if (executable) {
+      if (!executable.isReady()) {
+        executable.close();
+      }
+      executable.execute();
+    }
   }
 
   executeNext() {
-    this.snippet.executeNext();
-  }
-
-  dispose() {
-    while (this.disposals.length) {
-      this.disposals.pop()();
+    if (this.activeExecutable() && this.activeExecutable().nextExecutable) {
+      this.activeExecutable(this.activeExecutable().nextExecutable);
     }
+    this.execute();
   }
 }
 

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

@@ -62,10 +62,6 @@ 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 -->
@@ -128,12 +124,7 @@ 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 -->
+        <h1 class="empty">${ I18n('Select and execute a query to see the result.') }</h1>
       </div>
     </div>
   </div>
@@ -146,12 +137,6 @@ class SnippetResults extends DisposableComponent {
     this.element = element;
 
     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;
@@ -205,21 +190,21 @@ class SnippetResults extends DisposableComponent {
     });
 
     this.subscribe(EXECUTABLE_UPDATED_EVENT, executable => {
-      if (this.selectedExecutable() === executable) {
+      if (this.activeExecutable() === executable) {
         this.updateFromExecutable(executable);
       }
     });
 
     let lastRenderedResult = undefined;
     this.subscribe(RESULT_UPDATED_EVENT, executionResult => {
-      if (this.selectedExecutable() === executionResult.executable) {
+      if (this.activeExecutable() === executionResult.executable) {
         const refresh = lastRenderedResult !== executionResult;
         this.updateFromExecutionResult(executionResult, refresh);
         lastRenderedResult = executionResult;
       }
     });
 
-    this.subscribe(this.selectedExecutable, executable => {
+    this.subscribe(this.activeExecutable, executable => {
       if (executable && executable.result) {
         if (executable !== lastRenderedResult) {
           this.updateFromExecutionResult(executable.result, true);

+ 29 - 25
desktop/core/src/desktop/js/apps/notebook2/components/spec/ko.executableProgressBar.spec.js

@@ -1,61 +1,65 @@
 import huePubSub from 'utils/huePubSub';
 import { koSetup } from 'spec/jasmineSetup';
 import { NAME } from '../ko.executableProgressBar';
-import { EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
-import { EXECUTOR_UPDATED_EVENT } from 'apps/notebook2/execution/executor';
+import { EXECUTABLE_UPDATED_EVENT, EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
 
 describe('ko.executableProgressBar.js', () => {
   const setup = koSetup();
 
   it('should render component', async () => {
-    const element = await setup.renderComponent(NAME, {});
+    const mockExecutable = {
+      status: EXECUTION_STATUS.ready,
+      progress: 0
+    };
+    const activeExecutable = () => mockExecutable;
+    activeExecutable.prototype.subscribe = () => {};
+    const element = await setup.renderComponent(NAME, {
+      activeExecutable: activeExecutable
+    });
 
     expect(element.querySelector('[data-test="' + NAME + '"]')).toBeTruthy();
   });
 
   it('should reflect progress updates', async () => {
-    const mockExecutor = {};
-    const snippet = {
-      executor: mockExecutor
+    const mockExecutable = {
+      status: EXECUTION_STATUS.ready,
+      progress: 0
     };
+    const activeExecutable = () => mockExecutable;
+    activeExecutable.prototype.subscribe = () => {};
+
     const wrapper = await setup.renderComponent(NAME, {
-      snippet: snippet
+      activeExecutable: activeExecutable
     });
 
     // Progress should be 2% initially
     expect(wrapper.querySelector('[data-test="bar"]').style['width']).toEqual('2%');
 
-    huePubSub.publish(EXECUTOR_UPDATED_EVENT, {
-      executor: mockExecutor,
-      executable: {
-        status: EXECUTION_STATUS.running,
-        progress: 10
-      }
-    });
+    mockExecutable.status = EXECUTION_STATUS.running;
+    mockExecutable.progress = 10;
+    huePubSub.publish(EXECUTABLE_UPDATED_EVENT, mockExecutable);
     await setup.waitForKoUpdate();
 
     expect(wrapper.querySelector('[data-test="bar"]').style['width']).toEqual('10%');
   });
 
   it('should be 100% and have .progress-danger when failed', async () => {
-    const mockExecutor = {};
-    const snippet = {
-      executor: mockExecutor
+    const mockExecutable = {
+      status: EXECUTION_STATUS.ready,
+      progress: 0
     };
+    const activeExecutable = () => mockExecutable;
+    activeExecutable.prototype.subscribe = () => {};
     const wrapper = await setup.renderComponent(NAME, {
-      snippet: snippet
+      activeExecutable: activeExecutable
     });
 
     expect(wrapper.querySelector('[data-test="bar"]').style['width']).toEqual('2%');
     expect(wrapper.querySelector('[data-test="' + NAME + '"].progress-danger')).toBeFalsy();
 
-    huePubSub.publish(EXECUTOR_UPDATED_EVENT, {
-      executor: mockExecutor,
-      executable: {
-        status: EXECUTION_STATUS.failed,
-        progress: 10
-      }
-    });
+    mockExecutable.status = EXECUTION_STATUS.failed;
+    mockExecutable.progress = 10;
+    huePubSub.publish(EXECUTABLE_UPDATED_EVENT, mockExecutable);
     await setup.waitForKoUpdate();
 
     expect(wrapper.querySelector('[data-test="bar"]').style['width']).toEqual('100%');

+ 23 - 16
desktop/core/src/desktop/js/apps/notebook2/components/spec/ko.snippetExecuteActions.spec.js

@@ -1,14 +1,24 @@
 import huePubSub from 'utils/huePubSub';
 import { koSetup } from 'spec/jasmineSetup';
 import { NAME } from '../ko.snippetExecuteActions';
-import { EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
-import { EXECUTOR_UPDATED_EVENT } from 'apps/notebook2/execution/executor';
+import { EXECUTABLE_UPDATED_EVENT, EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
 
 describe('ko.snippetExecuteActions.js', () => {
   const setup = koSetup();
 
   it('should render component', async () => {
-    const element = await setup.renderComponent(NAME, {});
+    const mockExecutable = {
+      cancel: () => {},
+      execute: () => {},
+      isReady: () => true,
+      nextExecutable: {},
+      status: EXECUTION_STATUS.ready
+    };
+    const activeExecutable = () => mockExecutable;
+    activeExecutable.prototype.subscribe = () => {};
+    const element = await setup.renderComponent(NAME, {
+      activeExecutable: activeExecutable
+    });
 
     expect(element.querySelector('[data-test="' + NAME + '"]')).toBeTruthy();
   });
@@ -16,20 +26,21 @@ describe('ko.snippetExecuteActions.js', () => {
   it('should handle execute and stop clicks', async () => {
     let executeCalled = false;
     let cancelCalled = false;
-    const mockExecutor = {
+    const mockExecutable = {
       cancel: () => {
         cancelCalled = true;
       },
-      hasMoreToExecute: () => true
-    };
-    const snippet = {
-      executor: mockExecutor,
       execute: () => {
         executeCalled = true;
-      }
+      },
+      isReady: () => true,
+      nextExecutable: {},
+      status: EXECUTION_STATUS.ready
     };
+    const activeExecutable = () => mockExecutable;
+    activeExecutable.prototype.subscribe = () => {};
     const wrapper = await setup.renderComponent(NAME, {
-      snippet: snippet
+      activeExecutable: activeExecutable
     });
 
     // Click play
@@ -39,12 +50,8 @@ describe('ko.snippetExecuteActions.js', () => {
     wrapper.querySelector('[data-test="execute"]').click();
 
     expect(executeCalled).toBeTruthy();
-    huePubSub.publish(EXECUTOR_UPDATED_EVENT, {
-      executor: mockExecutor,
-      executable: {
-        status: EXECUTION_STATUS.running
-      }
-    });
+    mockExecutable.status = EXECUTION_STATUS.running;
+    huePubSub.publish(EXECUTABLE_UPDATED_EVENT, mockExecutable);
 
     await setup.waitForKoUpdate();
 

+ 19 - 12
desktop/core/src/desktop/js/apps/notebook2/execution/executable.js

@@ -44,19 +44,11 @@ export const EXECUTABLE_UPDATED_EVENT = 'hue.executable.updated';
 export default class Executable {
   /**
    * @param options
-   * @param {string} options.sourceType
-   * @param {ContextCompute} options.compute
-   * @param {ContextNamespace} options.namespace
    * @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 = {
@@ -72,6 +64,8 @@ export default class Executable {
 
     this.executeStarted = 0;
     this.executeEnded = 0;
+
+    this.nextExecutable = undefined;
   }
 
   setStatus(status) {
@@ -95,6 +89,10 @@ export default class Executable {
     }, 1);
   }
 
+  isReady() {
+    return this.status === EXECUTION_STATUS.ready;
+  }
+
   async execute() {
     if (this.status !== EXECUTION_STATUS.ready) {
       return;
@@ -105,8 +103,8 @@ export default class Executable {
     this.setProgress(0);
 
     try {
-      const session = await sessionManager.getSession({ type: this.sourceType });
-      hueAnalytics.log('notebook', 'execute/' + this.sourceType);
+      const session = await sessionManager.getSession({ type: this.executor.sourceType() });
+      hueAnalytics.log('notebook', 'execute/' + this.executor.sourceType());
       this.handle = await this.internalExecute(session);
 
       if (this.handle.has_result_set && this.handle.sync) {
@@ -189,9 +187,13 @@ export default class Executable {
     throw new Error('Implement in subclass!');
   }
 
+  getKey() {
+    throw new Error('Implement in subclass!');
+  }
+
   async cancel() {
     if (this.cancellables.length && this.status === EXECUTION_STATUS.running) {
-      hueAnalytics.log('notebook', 'cancel/' + this.sourceType);
+      hueAnalytics.log('notebook', 'cancel/' + this.executor.sourceType());
       this.setStatus(EXECUTION_STATUS.canceling);
       while (this.cancellables.length) {
         await this.cancellables.pop().cancel();
@@ -202,7 +204,12 @@ export default class Executable {
 
   async close() {
     while (this.cancellables.length) {
-      await this.cancellables.pop().cancel();
+      const nextCancellable = this.cancellables.pop();
+      try {
+        await nextCancellable.cancel();
+      } catch (err) {
+        console.warn(err);
+      }
     }
 
     return new Promise(resolve => {

+ 37 - 101
desktop/core/src/desktop/js/apps/notebook2/execution/executor.js

@@ -15,29 +15,11 @@
 // limitations under the License.
 
 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';
 
 // TODO: Remove, debug var
 window.sessionManager = sessionManager;
 
-const EXECUTION_FLOW = {
-  step: 'step',
-  batch: 'batch'
-  // batchNoBreak: 'batchNoBreak'
-};
-
-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
@@ -45,7 +27,6 @@ class Executor {
    * @param {string} options.sourceType
    * @param {ContextCompute} options.compute
    * @param {ContextNamespace} options.namespace
-   * @param {EXECUTION_FLOW} [options.executionFlow] (default EXECUTION_FLOW.batch)
    * @param {string} options.statement
    * @param {string} [options.database]
    */
@@ -55,98 +36,53 @@ 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;
-
-    this.toExecute = [];
-    this.currentExecutable = undefined;
-    this.executed = [];
-
-    huePubSub.subscribe(EXECUTABLE_UPDATED_EVENT, executable => {
-      if (
-        executable === this.currentExecutable ||
-        this.executed.some(executed => executed === executable)
-      ) {
-        huePubSub.publish(EXECUTOR_UPDATED_EVENT, {
-          executable: executable,
-          executor: this
-        });
-      }
-    });
+    this.executables = [];
   }
 
-  getExecutable(parsedStatement) {
-    return this.executed
-      .concat(this.currentExecutable, this.toExecute)
-      .find(
-        executable =>
-          executable &&
-          executable.parsedStatement &&
-          parsedStatementEquals(parsedStatement, executable.parsedStatement)
+  update(statementDetails) {
+    const newExecutables = statementDetails.precedingStatements
+      .concat(statementDetails.activeStatement, statementDetails.followingStatements)
+      .map(
+        parsedStatement =>
+          new SqlExecutable({
+            parsedStatement: parsedStatement,
+            database: this.database(),
+            executor: this
+          })
       );
-  }
-
-  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;
-  }
-
-  async cancel() {
-    if (this.isRunning()) {
-      return await this.currentExecutable.cancel();
-    }
-  }
+    const existingExecutableIndex = {};
+    this.executables.forEach(executable => {
+      existingExecutableIndex[executable.getKey()] = executable;
+    });
 
-  hasMoreToExecute() {
-    return !!this.toExecute.length;
-  }
+    let activeExecutable = new SqlExecutable({
+      parsedStatement: statementDetails.activeStatement,
+      database: this.database(),
+      executor: this
+    });
 
-  async executeNext() {
-    if (!this.toExecute.length) {
-      return;
+    if (existingExecutableIndex[activeExecutable.getKey()]) {
+      activeExecutable = existingExecutableIndex[activeExecutable.getKey()];
     }
 
-    this.currentExecutable = this.toExecute.shift();
-
-    await this.currentExecutable.execute();
-
-    this.executed.push(this.currentExecutable);
+    // 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;
+    });
 
-    if (this.canExecuteNextInBatch()) {
-      await this.executeNext();
-    }
-  }
+    // Cancel lost executables
+    Object.keys(existingExecutableIndex).forEach(key => {
+      existingExecutableIndex[key].cancel();
+    });
 
-  canExecuteNextInBatch() {
-    return (
-      !this.executed.length ||
-      (this.isSqlEngine() &&
-        this.executionFlow !== EXECUTION_FLOW.step &&
-        this.currentExecutable &&
-        this.currentExecutable.canExecuteInBatch())
-    );
+    return activeExecutable;
   }
 }
 

+ 2 - 4
desktop/core/src/desktop/js/apps/notebook2/execution/spec/sqlExecutableSpec.js

@@ -33,10 +33,8 @@ describe('sqlExecutable.js', () => {
   const createSubject = statement => {
     if (typeof statement === 'string') {
       return new SqlExecutable({
-        compute: { id: 'compute' },
-        namespace: { id: 'namespace' },
         database: 'default',
-        statement: statement,
+        parsedStatement: { statement: statement },
         sourceType: 'impala'
       });
     }
@@ -84,7 +82,7 @@ describe('sqlExecutable.js', () => {
     simplePostDeferred.resolve({ handle: {} });
   });
 
-  it('should set the correct status after failed execute', done => {
+  xit('should set the correct status after failed execute', done => {
     const subject = createSubject('SELECT * FROM customers');
 
     const simplePostDeferred = $.Deferred();

+ 7 - 8
desktop/core/src/desktop/js/apps/notebook2/execution/sqlExecutable.js

@@ -23,20 +23,15 @@ const BATCHABLE_STATEMENT_TYPES = /ALTER|CREATE|DELETE|DROP|GRANT|INSERT|INVALID
 export default class SqlExecutable extends Executable {
   /**
    * @param options
-   * @param {string} options.sourceType
-   * @param {ContextCompute} options.compute
-   * @param {ContextNamespace} options.namespace
-   * @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]
+   *
+   * @param {string} options.database
+   * @param {SqlStatementsParserResult} options.parsedStatement
    */
   constructor(options) {
     super(options);
     this.database = options.database;
     this.parsedStatement = options.parsedStatement;
-    this.statement = options.statement;
   }
 
   getStatement() {
@@ -50,6 +45,10 @@ export default class SqlExecutable extends Executable {
     });
   }
 
+  getKey() {
+    return this.database + '_' + this.parsedStatement.statement;
+  }
+
   /**
    *
    * @param options

+ 65 - 72
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 Executor, { EXECUTOR_UPDATED_EVENT } from 'apps/notebook2/execution/executor';
+import Executor from 'apps/notebook2/execution/executor';
 import hueAnalytics from 'utils/hueAnalytics';
 import huePubSub from 'utils/huePubSub';
 import hueUtils from 'utils/hueUtils';
@@ -35,6 +35,12 @@ 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 {
+  ACTIVE_STATEMENT_CHANGED_EVENT,
+  REFRESH_STATEMENT_LOCATIONS_EVENT
+} from 'sql/aceLocationHandler';
+import { EXECUTE_ACTIVE_EXECUTABLE_EVENT } from 'apps/notebook2/components/ko.snippetExecuteActions';
 
 // TODO: Remove. Temporary here for debug
 window.SqlExecutable = SqlExecutable;
@@ -364,7 +370,7 @@ export default class Snippet {
     this.statementsList = ko.observableArray();
 
     huePubSub.subscribe(
-      'editor.active.statement.changed',
+      ACTIVE_STATEMENT_CHANGED_EVENT,
       statementDetails => {
         if (this.ace() && this.ace().container.id === statementDetails.id) {
           for (let i = statementDetails.precedingStatements.length - 1; i >= 0; i--) {
@@ -381,13 +387,9 @@ export default class Snippet {
               break;
             }
           }
-          if (statementDetails.activeStatement) {
-            this.positionStatement(statementDetails.activeStatement);
-            this.activeExecutable(this.executor.getExecutable(statementDetails.activeStatement));
-          } else {
-            this.positionStatement(null);
-            this.activeExecutable(undefined);
-          }
+
+          this.positionStatement(statementDetails.activeStatement);
+          this.activeExecutable(this.executor.update(statementDetails));
 
           if (statementDetails.activeStatement) {
             const _statements = [];
@@ -996,7 +998,6 @@ export default class Snippet {
       timeout: this.parentVm.autocompleteTimeout
     });
 
-    this.latestExecutable = ko.observable();
     this.activeExecutable = ko.observable();
 
     this.executor = new Executor({
@@ -1004,22 +1005,18 @@ export default class Snippet {
       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) {
-        this.latestExecutable(executable);
-        this.activeExecutable(executable); // TODO: Move to pureComputed (based on cursor)
+    huePubSub.subscribe(EXECUTABLE_UPDATED_EVENT, executable => {
+      if (this.activeExecutable() === executable) {
         this.status(executable.status);
-        this.progress(executable.progress);
       }
     });
 
     this.refreshHistory = notebook.fetchHistory;
+
+    huePubSub.publish(REFRESH_STATEMENT_LOCATIONS_EVENT, this);
   }
 
   ace(newVal) {
@@ -1048,60 +1045,56 @@ export default class Snippet {
     });
   }
 
-  async executeNext() {
-    hueAnalytics.log('notebook', 'execute/' + this.type());
-
-    const now = new Date().getTime();
-    if (now - this.lastExecuted() < 1000) {
-      return; // Prevent fast clicks
-    }
-    this.lastExecuted(now);
-
-    if (this.type() === TYPE.impala) {
-      this.showExecutionAnalysis(false);
-      huePubSub.publish('editor.clear.execution.analysis');
-    }
-
-    // Editor based execution
-    if (this.ace()) {
-      const selectionRange = this.ace().getSelectionRange();
-
-      if (this.isSqlDialect()) {
-        huePubSub.publish('editor.refresh.statement.locations', this);
-      }
-
-      huePubSub.publish('ace.set.autoexpand', { autoExpand: false, snippet: this });
-      this.lastAceSelectionRowOffset(Math.min(selectionRange.start.row, selectionRange.end.row));
-    }
-
-    const $snip = $('#snippet_' + this.id());
-    $snip.find('.progress-snippet').animate(
-      {
-        height: '3px'
-      },
-      100
-    );
-
-    $('.jHueNotify').remove();
-    this.parentNotebook.forceHistoryInitialHeight(true);
-    this.errors([]);
-    huePubSub.publish('editor.clear.highlighted.errors', this.ace());
-    this.progress(0);
-    this.jobs([]);
-
-    this.parentNotebook.historyCurrentPage(1);
-
-    this.startLongOperationTimeout();
-
-    try {
-      await this.executor.executeNext();
-    } catch (error) {}
-    this.stopLongOperationTimeout();
-  }
-
-  async execute() {
-    await this.executor.reset();
-    await this.executeNext();
+  // async executeNext() {
+  //   hueAnalytics.log('notebook', 'execute/' + this.type());
+  //
+  //   const now = new Date().getTime();
+  //   if (now - this.lastExecuted() < 1000) {
+  //     return; // Prevent fast clicks
+  //   }
+  //   this.lastExecuted(now);
+  //
+  //   if (this.type() === TYPE.impala) {
+  //     this.showExecutionAnalysis(false);
+  //     huePubSub.publish('editor.clear.execution.analysis');
+  //   }
+  //
+  //   // Editor based execution
+  //   if (this.ace()) {
+  //     const selectionRange = this.ace().getSelectionRange();
+  //
+  //     huePubSub.publish('ace.set.autoexpand', { autoExpand: false, snippet: this });
+  //     this.lastAceSelectionRowOffset(Math.min(selectionRange.start.row, selectionRange.end.row));
+  //   }
+  //
+  //   const $snip = $('#snippet_' + this.id());
+  //   $snip.find('.progress-snippet').animate(
+  //     {
+  //       height: '3px'
+  //     },
+  //     100
+  //   );
+  //
+  //   $('.jHueNotify').remove();
+  //   this.parentNotebook.forceHistoryInitialHeight(true);
+  //   this.errors([]);
+  //   huePubSub.publish('editor.clear.highlighted.errors', this.ace());
+  //   this.progress(0);
+  //   this.jobs([]);
+  //
+  //   this.parentNotebook.historyCurrentPage(1);
+  //
+  //   this.startLongOperationTimeout();
+  //
+  //   try {
+  //     await this.executor.executeNext();
+  //   } catch (error) {}
+  //   this.stopLongOperationTimeout();
+  // }
+
+  execute() {
+    // From ctrl + enter
+    huePubSub.publish(EXECUTE_ACTIVE_EXECUTABLE_EVENT, this.activeExecutable());
   }
 
   async exportHistory() {

+ 13 - 13
desktop/core/src/desktop/js/sql/aceLocationHandler.js

@@ -27,6 +27,9 @@ import stringDistance from 'sql/stringDistance';
 
 // TODO: depends on Ace, sqlStatementsParser
 
+export const REFRESH_STATEMENT_LOCATIONS_EVENT = 'editor.refresh.statement.locations';
+export const ACTIVE_STATEMENT_CHANGED_EVENT = 'editor.active.statement.changed';
+
 const STATEMENT_COUNT_AROUND_ACTIVE = 10;
 
 const VERIFY_LIMIT = 50;
@@ -568,7 +571,7 @@ class AceLocationHandler {
         }
       }
 
-      huePubSub.publish('editor.active.statement.changed', {
+      huePubSub.publish(ACTIVE_STATEMENT_CHANGED_EVENT, {
         id: self.editorId,
         editorChangeTime: lastKnownStatements.editorChangeTime,
         activeStatementIndex: statementIndex,
@@ -650,19 +653,16 @@ class AceLocationHandler {
       }
     });
 
-    const locateSubscription = huePubSub.subscribe(
-      'editor.refresh.statement.locations',
-      snippet => {
-        if (snippet === self.snippet) {
-          cursorChangePaused = true;
-          window.clearTimeout(changeThrottle);
-          window.clearTimeout(updateThrottle);
-          parseForStatements();
-          updateActiveStatement();
-          cursorChangePaused = false;
-        }
+    const locateSubscription = huePubSub.subscribe(REFRESH_STATEMENT_LOCATIONS_EVENT, snippet => {
+      if (snippet === self.snippet) {
+        cursorChangePaused = true;
+        window.clearTimeout(changeThrottle);
+        window.clearTimeout(updateThrottle);
+        parseForStatements();
+        updateActiveStatement();
+        cursorChangePaused = false;
       }
-    );
+    });
 
     self.disposeFunctions.push(() => {
       window.clearTimeout(changeThrottle);

+ 2 - 4
desktop/libs/notebook/src/notebook/templates/editor_components2.mako

@@ -671,7 +671,6 @@
           <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: {
-                latestExecutable: latestExecutable,
                 activeExecutable: activeExecutable,
                 editorMode: parentVm.editorMode,
                 id: id,
@@ -825,7 +824,6 @@
             <!-- /ko -->
             <!-- ko if: !$root.editorMode() && ['text', 'jar', 'java', 'distcp', 'shell', 'mapreduce', 'py', 'markdown'].indexOf(type()) === -1 -->
               <!-- ko component: { name: 'snippet-results', params: {
-                latestExecutable: latestExecutable,
                 activeExecutable: activeExecutable,
                 editorMode: parentVm.editorMode,
                 id: id,
@@ -1274,7 +1272,7 @@
           css: {'blue': $data.showLogs}
         " title="${ _('Toggle Logs') }"><i class="fa fa-fw" data-bind="css: { 'fa-caret-right': !$data.showLogs(), 'fa-caret-down': $data.showLogs() }"></i></a>
       <div class="snippet-progress-container" data-bind="visible: status() != 'canceled' && status() != 'with-optimizer-report'">
-        <!-- ko component: { name: 'executable-progress-bar', params: { snippet: $data } } --><!-- /ko -->
+        <!-- ko component: { name: 'executable-progress-bar', params: { activeExecutable: activeExecutable } } --><!-- /ko -->
       </div>
       <div class="snippet-error-container alert alert-error" style="margin-bottom: 0" data-bind="visible: errors().length > 0">
         <ul class="unstyled" data-bind="foreach: errors">
@@ -1337,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: { snippet: $data } }" />
+      <div class="pull-left" data-bind="component: { name: 'snippet-execute-actions', params: { activeExecutable: activeExecutable } }" />
       <!-- ko if: isSqlDialect() && !$root.isPresentationMode() -->
       <div class="pull-right" data-bind="component: { name: 'snippet-editor-actions', params: { snippet: $data } }" />
       <!-- /ko -->