Quellcode durchsuchen

HUE-9456 [editor] Clean up editor V2 execution logic and enable roundtrip test

This takes care of a bunch of bugs and removes unused logic, now everything is contained in the notebook2 src folder.
Johan Ahlen vor 5 Jahren
Ursprung
Commit
24578d96b8
31 geänderte Dateien mit 1550 neuen und 1169 gelöschten Zeilen
  1. 33 346
      desktop/core/src/desktop/js/api/apiHelper.js
  2. 122 0
      desktop/core/src/desktop/js/api/apiUtilsV2.ts
  3. 126 0
      desktop/core/src/desktop/js/api/cancellableJqPromise.ts
  4. 18 101
      desktop/core/src/desktop/js/api/cancellablePromise.ts
  5. 9 0
      desktop/core/src/desktop/js/api/urls.js
  6. 383 0
      desktop/core/src/desktop/js/apps/notebook2/execution/apiUtils.ts
  7. 199 174
      desktop/core/src/desktop/js/apps/notebook2/execution/executable.ts
  8. 23 16
      desktop/core/src/desktop/js/apps/notebook2/execution/executionLogs.ts
  9. 58 57
      desktop/core/src/desktop/js/apps/notebook2/execution/executionResult.ts
  10. 0 59
      desktop/core/src/desktop/js/apps/notebook2/execution/executor.test.js
  11. 34 25
      desktop/core/src/desktop/js/apps/notebook2/execution/executor.ts
  12. 73 30
      desktop/core/src/desktop/js/apps/notebook2/execution/sessionManager.test.ts
  13. 41 63
      desktop/core/src/desktop/js/apps/notebook2/execution/sessionManager.ts
  14. 0 155
      desktop/core/src/desktop/js/apps/notebook2/execution/sqlExecutable.test.js
  15. 213 0
      desktop/core/src/desktop/js/apps/notebook2/execution/sqlExecutable.test.ts
  16. 52 24
      desktop/core/src/desktop/js/apps/notebook2/execution/sqlExecutable.ts
  17. 31 23
      desktop/core/src/desktop/js/apps/notebook2/execution/utils.test.ts
  18. 21 13
      desktop/core/src/desktop/js/apps/notebook2/execution/utils.ts
  19. 2 2
      desktop/core/src/desktop/js/catalog/catalogUtils.js
  20. 5 5
      desktop/core/src/desktop/js/catalog/dataCatalog.js
  21. 37 37
      desktop/core/src/desktop/js/catalog/dataCatalogEntry.js
  22. 11 11
      desktop/core/src/desktop/js/catalog/multiTableEntry.js
  23. 5 5
      desktop/core/src/desktop/js/catalog/optimizer/apiStrategy.js
  24. 13 13
      desktop/core/src/desktop/js/catalog/optimizer/baseStrategy.js
  25. 2 2
      desktop/core/src/desktop/js/hue.js
  26. 12 0
      desktop/core/src/desktop/js/parse/sqlStatementsParser.d.ts
  27. 15 0
      desktop/core/src/desktop/js/parse/types.ts
  28. 4 4
      desktop/core/src/desktop/js/sql/sqlUtils.ts
  29. 4 0
      desktop/core/src/desktop/js/utils/json.bigDataParse.d.ts
  30. 3 3
      package-lock.json
  31. 1 1
      tsconfig.json

+ 33 - 346
desktop/core/src/desktop/js/api/apiHelper.js

@@ -25,7 +25,7 @@ import {
   successResponseIsError
 } from './apiUtils';
 import apiQueueManager from 'api/apiQueueManager';
-import CancellablePromise from 'api/cancellablePromise';
+import CancellableJqPromise from 'api/cancellableJqPromise';
 import hueDebug from 'utils/hueDebug';
 import huePubSub from 'utils/huePubSub';
 import hueUtils from 'utils/hueUtils';
@@ -1032,7 +1032,7 @@ class ApiHelper {
    * @param {boolean} [options.silenceErrors]
    * @param {boolean} [options.fetchContents]
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   fetchDocument(options) {
     const deferred = $.Deferred();
@@ -1060,7 +1060,7 @@ class ApiHelper {
         errorCallback: deferred.reject
       })
     );
-    return new CancellablePromise(deferred, request);
+    return new CancellableJqPromise(deferred, request);
   }
 
   /**
@@ -1312,7 +1312,7 @@ class ApiHelper {
         .done(deferred.resolve)
         .fail(deferred.reject);
 
-      return new CancellablePromise(deferred, request);
+      return new CancellableJqPromise(deferred, request);
     }
 
     return deferred.resolve().promise();
@@ -1327,7 +1327,7 @@ class ApiHelper {
    *
    * @param {string[]} [options.path] - The path to fetch
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   fetchSourceMetadata(options) {
     const deferred = $.Deferred();
@@ -1379,7 +1379,7 @@ class ApiHelper {
         })
       );
 
-    return new CancellablePromise(deferred, request);
+    return new CancellableJqPromise(deferred, request);
   }
 
   updateSourceMetadata(options) {
@@ -1431,7 +1431,7 @@ class ApiHelper {
    * @param {string} options.sourceType
    * @param {string[]} options.path
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   fetchAnalysis(options) {
     const deferred = $.Deferred();
@@ -1469,7 +1469,7 @@ class ApiHelper {
       errorCallback: deferred.reject
     });
 
-    return new CancellablePromise(deferred, request);
+    return new CancellableJqPromise(deferred, request);
   }
 
   /**
@@ -1481,7 +1481,7 @@ class ApiHelper {
    * @param {string[]} options.path
    * @param {ContextCompute} options.compute
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   fetchPartitions(options) {
     const deferred = $.Deferred();
@@ -1525,7 +1525,7 @@ class ApiHelper {
         }
       });
 
-    return new CancellablePromise(deferred, request);
+    return new CancellableJqPromise(deferred, request);
   }
 
   /**
@@ -1538,7 +1538,7 @@ class ApiHelper {
    * @param {ContextCompute} options.compute
    * @param {string[]} options.path
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   refreshAnalysis(options) {
     if (options.path.length === 1) {
@@ -1593,7 +1593,7 @@ class ApiHelper {
       })
     );
 
-    return new CancellablePromise(deferred, undefined, promises);
+    return new CancellableJqPromise(deferred, undefined, promises);
   }
 
   /**
@@ -1605,7 +1605,7 @@ class ApiHelper {
    * @param {Object} options.snippetJson
    * @param {boolean} [options.silenceErrors]
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   whenAvailable(options) {
     const deferred = $.Deferred();
@@ -1645,11 +1645,11 @@ class ApiHelper {
         })
         .fail(deferred.reject);
 
-      cancellablePromises.push(new CancellablePromise(request, request));
+      cancellablePromises.push(new CancellableJqPromise(request, request));
     };
 
     waitForAvailable();
-    return new CancellablePromise(deferred, undefined, cancellablePromises);
+    return new CancellableJqPromise(deferred, undefined, cancellablePromises);
   }
 
   clearNotebookHistory(options) {
@@ -1669,43 +1669,6 @@ class ApiHelper {
     return simplePost('/notebook/api/notebook/close', data);
   }
 
-  /**
-   * Creates a new session
-   *
-   * @param {Object} options
-   * @param {String} options.type
-   * @param {Array<SessionProperty>} [options.properties] - Default []
-   * @return {Promise<Session>}
-   */
-  async createSession(options) {
-    return new Promise((resolve, reject) => {
-      const data = {
-        session: JSON.stringify({ type: options.type, properties: options.properties || [] })
-      };
-      $.post({
-        url: '/notebook/api/create_session',
-        data: data
-      })
-        .done(data => {
-          if (data.status === 401) {
-            resolve({ auth: true, message: data.message });
-          } else if (successResponseIsError(data)) {
-            reject(assistErrorCallback(options)(data));
-          } else {
-            resolve(data.session);
-          }
-        })
-        .fail(assistErrorCallback(options));
-    });
-  }
-
-  closeSession(options) {
-    const data = {
-      session: JSON.stringify(options.session)
-    };
-    return simplePost('/notebook/api/close_session', data, options);
-  }
-
   async checkStatus(options) {
     return new Promise((resolve, reject) => {
       const data = {
@@ -1787,98 +1750,12 @@ class ApiHelper {
     });
   }
 
-  /**
-   * @typedef {Object} ExecutionHandle
-   * @property {string} guid
-   * @property {boolean} has_more_statements
-   * @property {boolean} has_result_set
-   * @property {Object} log_context
-   * @property {number} modified_row_count
-   * @property {number} operation_type
-   * @property {string} previous_statement_hash
-   * @property {string} secret
-   * @property {string} session_guid
-   * @property {string} statement
-   * @property {number} statement_id
-   * @property {number} statements_count
-   */
-
-  /**
-   * API function to execute an SqlExecutable
-   *
-   * @param {Object} options
-   * @param {boolean} [options.silenceErrors]
-   * @param {SqlExecutable} options.executable
-   * @param {Session} options.session
-   *
-   * @return {Promise<ExecutionHandle>}
-   */
-  async executeStatement(options) {
-    const executable = options.executable;
-    const url = URLS.EXECUTE_API_PREFIX + executable.executor.connector().dialect;
-
-    const promise = new Promise(async (resolve, reject) => {
-      let data = {};
-      if (executable.executor.snippet) {
-        // V1
-        // TODO: Refactor away the snippet, it currently works because snippet.statement is a computed from
-        // the active executable, but we n
-        data = {
-          notebook: await executable.executor.snippet.parentNotebook.toJson(),
-          snippet: executable.executor.snippet.toContextJson()
-        };
-      } else {
-        data = await executable.toContext();
-      }
-
-      data.executable = executable.toJson();
-
-      simplePost(url, data, options)
-        .done(response => {
-          const executeResponse = {};
-          if (response.handle) {
-            executeResponse.handle = response.handle;
-            executeResponse.handle.result = response.result;
-          } else {
-            reject('No handle in execute response');
-            return;
-          }
-          if (response.history_id) {
-            executeResponse.history = {
-              id: response.history_id,
-              uuid: response.history_uuid,
-              parentUuid: response.history_parent_uuid
-            };
-          }
-          resolve(executeResponse);
-        })
-        .fail(reject);
-    });
-
-    executable.addCancellable({
-      cancel: async () => {
-        try {
-          const handle = await promise;
-          if (options.executable.handle !== handle) {
-            options.executable.handle = handle;
-          }
-          await this.cancelStatement(options);
-        } catch (err) {
-          console.warn('Failed cancelling statement');
-          console.warn(err);
-        }
-      }
-    });
-
-    return promise;
-  }
-
   /**
    *
    * @param {Object} options
    * @param {Snippet} options.snippet
    *
-   * @return {CancellablePromise<string>}
+   * @return {CancellableJqPromise<string>}
    */
   async explainAsync(options) {
     const data = {
@@ -1902,7 +1779,7 @@ class ApiHelper {
    * @param {name} options.name
    * @param {description} options.description
    *
-   * @return {CancellablePromise<string>}
+   * @return {CancellableJqPromise<string>}
    */
   async createGistAsync(options) {
     const data = {
@@ -1920,196 +1797,6 @@ class ApiHelper {
     });
   }
 
-  /**
-   *
-   * @param {Object} options
-   * @param {boolean} [options.silenceErrors]
-   * @param {Executable} options.executable
-   *
-   * @return {CancellablePromise<string>}
-   */
-  checkExecutionStatus(options) {
-    const deferred = $.Deferred();
-
-    const request = $.post({
-      url: '/notebook/api/check_status',
-      data: {
-        operationId: options.executable.operationId
-      }
-    })
-      .done(response => {
-        if (response && response.query_status) {
-          deferred.resolve(response.query_status);
-        } else if (response && response.status === -3) {
-          deferred.resolve({ status: EXECUTION_STATUS.expired });
-        } else {
-          deferred.resolve({ status: EXECUTION_STATUS.failed, message: response.message });
-        }
-      })
-      .fail(err => {
-        deferred.reject(assistErrorCallback(options)(err));
-      });
-
-    return new CancellablePromise(deferred, request);
-  }
-
-  /**
-   *
-   * @param {Object} options
-   * @param {boolean} [options.silenceErrors]
-   * @param {Executable} options.executable
-   * @param {number} [options.from]
-   * @param {Object[]} [options.jobs]
-   * @param {string} options.fullLog
-   *
-   * @return {Promise<?>}
-   */
-  fetchLogs(options) {
-    return new Promise(async (resolve, reject) => {
-      const data = await options.executable.toContext();
-      data.full_log = options.fullLog;
-      data.jobs = options.jobs && JSON.stringify(options.jobs);
-      data.from = options.from || 0;
-      data.operationId = options.executable.operationId;
-      const request = simplePost('/notebook/api/get_logs', data, options)
-        .done(response => {
-          resolve({
-            logs: (response.status === 1 && response.message) || response.logs || '',
-            jobs: response.jobs || [],
-            isFullLogs: response.isFullLogs
-          });
-        })
-        .fail(reject);
-
-      options.executable.addCancellable({
-        cancel: () => {
-          cancelActiveRequest(request);
-        }
-      });
-    });
-  }
-
-  /**
-   *
-   * @param {Object} options
-   * @param {boolean} [options.silenceErrors]
-   * @param {SqlExecutable} options.executable
-   *
-   * @return {Promise}
-   */
-  async cancelStatement(options) {
-    return new Promise(async (resolve, reject) => {
-      simplePost('/notebook/api/cancel_statement', await options.executable.toContext(), options)
-        .done(resolve)
-        .fail(reject);
-    });
-  }
-
-  /**
-   * @typedef {Object} ResultResponseMeta
-   * @property {string} comment
-   * @property {string} name
-   * @property {string} type
-   */
-
-  /**
-   * @typedef {Object} ResultResponse
-   * @property {Object[]} data
-   * @property {boolean} has_more
-   * @property {boolean} isEscaped
-   * @property {ResultResponseMeta[]} meta
-   * @property {string} type
-   */
-
-  /**
-   *
-   * @param {Object} options
-   * @param {boolean} [options.silenceErrors]
-   * @param {SqlExecutable} options.executable
-   * @param {number} options.rows
-   * @param {boolean} options.startOver
-   *
-   * @return {Promise<ResultResponse>}
-   */
-  async fetchResults(options) {
-    return new Promise(async (resolve, reject) => {
-      const data = await options.executable.toContext();
-      data.rows = options.rows;
-      data.startOver = !!options.startOver;
-
-      const request = simplePost(
-        '/notebook/api/fetch_result_data',
-        data,
-        {
-          silenceErrors: options.silenceErrors,
-          dataType: 'text'
-        },
-        options
-      )
-        .done(response => {
-          const data = JSON.bigdataParse(response);
-          resolve(data.result);
-        })
-        .fail(reject);
-
-      options.executable.addCancellable({
-        cancel: () => {
-          cancelActiveRequest(request);
-        }
-      });
-    });
-  }
-
-  /**
-   *
-   * @param {Object} options
-   * @param {boolean} [options.silenceErrors]
-   * @param {Executable} options.executable
-   *
-   * @return {Promise<ResultResponse>}
-   */
-  async fetchResultSize2(options) {
-    return new Promise(async (resolve, reject) => {
-      const request = simplePost(
-        '/notebook/api/fetch_result_size',
-        await options.executable.toContext(),
-        options
-      )
-        .done(response => {
-          resolve(response.result);
-        })
-        .fail(reject);
-
-      options.executable.addCancellable({
-        cancel: () => {
-          cancelActiveRequest(request);
-        }
-      });
-    });
-  }
-
-  /**
-   *
-   * @param {Object} options
-   * @param {boolean} [options.silenceErrors]
-   * @param {SqlExecutable} options.executable
-   *
-   * @return {Promise}
-   */
-  async closeStatement(options) {
-    return new Promise(async (resolve, reject) => {
-      simplePost(
-        '/notebook/api/close_statement',
-        {
-          operationId: options.executable.operationId
-        },
-        options
-      )
-        .done(resolve)
-        .fail(reject);
-    });
-  }
-
   /**
    * Fetches samples for the given source and path
    *
@@ -2122,7 +1809,7 @@ class ApiHelper {
    * @param {string[]} options.path
    * @param {string} [options.operation] - Default 'default'
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   fetchSample(options) {
     const deferred = $.Deferred();
@@ -2234,7 +1921,7 @@ class ApiHelper {
       cancel: cancelQuery
     });
 
-    return new CancellablePromise(deferred, undefined, cancellablePromises);
+    return new CancellableJqPromise(deferred, undefined, cancellablePromises);
   }
 
   /**
@@ -2246,7 +1933,7 @@ class ApiHelper {
    * @param {boolean} [options.isView] - Default false
    * @param {string[]} options.path
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   fetchNavigatorMetadata(options) {
     const deferred = $.Deferred();
@@ -2270,7 +1957,7 @@ class ApiHelper {
         '&name=' +
         options.path[2];
     } else {
-      return new CancellablePromise($.Deferred().reject());
+      return new CancellableJqPromise($.Deferred().reject());
     }
 
     const request = simplePost(
@@ -2292,7 +1979,7 @@ class ApiHelper {
       }
     );
 
-    return new CancellablePromise(deferred, request);
+    return new CancellableJqPromise(deferred, request);
   }
 
   /**
@@ -2328,7 +2015,7 @@ class ApiHelper {
    * @param {Object} options
    * @param {boolean} [options.silenceErrors]
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   fetchAllNavigatorTags(options) {
     const deferred = $.Deferred();
@@ -2345,7 +2032,7 @@ class ApiHelper {
       errorCallback: deferred.reject
     });
 
-    return new CancellablePromise(deferred, request);
+    return new CancellableJqPromise(deferred, request);
   }
 
   addNavTags(entityId, tags) {
@@ -2367,7 +2054,7 @@ class ApiHelper {
    * @param {boolean} [options.silenceErrors]
    * @param {ContextCompute} options.compute
    * @param {string} options.queryId
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   fetchQueryExecutionAnalysis(options) {
     //var url = '/metadata/api/workload_analytics/get_impala_query/';
@@ -2378,7 +2065,7 @@ class ApiHelper {
 
     const cancellablePromises = [];
 
-    const promise = new CancellablePromise(deferred, undefined, cancellablePromises);
+    const promise = new CancellableJqPromise(deferred, undefined, cancellablePromises);
 
     const pollForAnalysis = () => {
       if (tries === 10) {
@@ -2442,7 +2129,7 @@ class ApiHelper {
       }
     );
 
-    return new CancellablePromise(deferred, request);
+    return new CancellableJqPromise(deferred, request);
   }
 
   fetchQueryExecutionStatistics(options) {
@@ -2468,7 +2155,7 @@ class ApiHelper {
       }
     );
 
-    return new CancellablePromise(deferred, request);
+    return new CancellableJqPromise(deferred, request);
   }
 
   /**
@@ -2533,7 +2220,7 @@ class ApiHelper {
         }
       })
       .fail(deferred.reject);
-    return new CancellablePromise(deferred, request);
+    return new CancellableJqPromise(deferred, request);
   }
 
   fetchNavEntitiesInteractive(options) {
@@ -2552,7 +2239,7 @@ class ApiHelper {
         }
       })
       .fail(deferred.reject);
-    return new CancellablePromise(deferred, request);
+    return new CancellableJqPromise(deferred, request);
   }
 
   searchEntities(options) {
@@ -2573,7 +2260,7 @@ class ApiHelper {
       }
     );
 
-    return new CancellablePromise(deferred, request);
+    return new CancellableJqPromise(deferred, request);
   }
 
   /**
@@ -2597,7 +2284,7 @@ class ApiHelper {
       }
     );
 
-    return new CancellablePromise(deferred, request);
+    return new CancellableJqPromise(deferred, request);
   }
 
   /**
@@ -2627,7 +2314,7 @@ class ApiHelper {
       }
     );
 
-    return new CancellablePromise(deferred, request);
+    return new CancellableJqPromise(deferred, request);
   }
 }
 

+ 122 - 0
desktop/core/src/desktop/js/api/apiUtilsV2.ts

@@ -0,0 +1,122 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import hueUtils from 'utils/hueUtils';
+import { CancellablePromise } from './cancellablePromise';
+
+export const successResponseIsError = (response?: {
+  traceback?: string;
+  status?: number;
+  code?: number;
+}): boolean => {
+  return (
+    typeof response !== 'undefined' &&
+    (typeof response.traceback !== 'undefined' ||
+      (typeof response.status !== 'undefined' && response.status !== 0) ||
+      response.code === 503 ||
+      response.code === 500)
+  );
+};
+
+const UNKNOWN_ERROR_MESSAGE = 'Unknown error occurred';
+
+export const extractErrorMessage = (
+  errorResponse?:
+    | {
+        statusText?: string;
+        responseText?: string;
+        message?: string;
+        error?: string | unknown;
+      }
+    | string
+): string => {
+  if (!errorResponse) {
+    return UNKNOWN_ERROR_MESSAGE;
+  }
+  if (typeof errorResponse === 'string') {
+    return errorResponse;
+  }
+  if (errorResponse.statusText && errorResponse.statusText !== 'abort') {
+    return errorResponse.statusText;
+  }
+  if (errorResponse.responseText) {
+    try {
+      const errorJs = JSON.parse(errorResponse.responseText);
+      if (errorJs.message) {
+        return errorJs.message;
+      }
+    } catch (err) {}
+    return errorResponse.responseText;
+  }
+  if (errorResponse.message) {
+    return errorResponse.message;
+  }
+  if (errorResponse.statusText) {
+    return errorResponse.statusText;
+  }
+  if (errorResponse.error && typeof errorResponse.error === 'string') {
+    return errorResponse.error;
+  }
+  return UNKNOWN_ERROR_MESSAGE;
+};
+
+export const simplePost = <T, U>(
+  url: string,
+  data?: U,
+  options?: { dataType?: string; silenceErrors?: boolean; ignoreSuccessErrors?: boolean }
+): CancellablePromise<T> =>
+  new CancellablePromise((resolve, reject, onCancel) => {
+    const handleErrorResponse = (data: never): void => {
+      const errorMessage = extractErrorMessage(data);
+      reject(errorMessage);
+      if (!options || !options.silenceErrors) {
+        hueUtils.logError(data);
+        if (errorMessage.indexOf('AuthorizationException') === -1) {
+          $(document).trigger('error', errorMessage);
+        }
+      }
+      reject(errorMessage);
+    };
+
+    const request = $.post({
+      url: url,
+      data: data,
+      dataType: options && options.dataType
+    })
+      .done(data => {
+        if ((!options || !options.ignoreSuccessErrors) && successResponseIsError(data)) {
+          handleErrorResponse(data as never);
+        } else {
+          resolve(data);
+        }
+      })
+      .fail(err => {
+        handleErrorResponse(err as never);
+      });
+
+    if (onCancel) {
+      onCancel(() => {
+        cancelActiveRequest(request);
+      });
+    }
+  });
+
+export const cancelActiveRequest = (request?: JQuery.jqXHR): void => {
+  if (request && request.readyState < 4) {
+    request.abort();
+  }
+};

+ 126 - 0
desktop/core/src/desktop/js/api/cancellableJqPromise.ts

@@ -0,0 +1,126 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import { cancelActiveRequest } from './apiUtils';
+
+export default class CancellableJqPromise<T> {
+  cancelCallbacks: (() => void)[] = [];
+  deferred: JQuery.Deferred<T>;
+  request?: JQuery.jqXHR;
+  otherCancellables?: CancellableJqPromise<unknown>[];
+  cancelled = false;
+  cancelPrevented = false;
+
+  constructor(
+    deferred: JQuery.Deferred<T>,
+    request?: JQuery.jqXHR,
+    otherCancellables?: CancellableJqPromise<unknown>[]
+  ) {
+    this.deferred = deferred;
+    this.request = request;
+    this.otherCancellables = otherCancellables;
+  }
+
+  /**
+   * A promise might be shared across multiple components in the UI, in some cases cancel is not an option and calling
+   * this will prevent that to happen.
+   *
+   * One example is autocompletion of databases while the assist is loading the database tree, closing the autocomplete
+   * results would make the assist loading fail if cancel hasn't been prevented.
+   *
+   * @returns {CancellableJqPromise}
+   */
+  preventCancel(): CancellableJqPromise<T> {
+    this.cancelPrevented = true;
+    return this;
+  }
+
+  cancel(): JQuery.Promise<unknown> {
+    if (this.cancelPrevented || this.cancelled || this.state() !== 'pending') {
+      return $.Deferred().resolve().promise();
+    }
+
+    this.cancelled = true;
+    if (this.request) {
+      cancelActiveRequest(this.request);
+    }
+
+    if (this.state && this.state() === 'pending' && this.deferred.reject) {
+      this.deferred.reject();
+    }
+
+    const cancelPromises: JQuery.Promise<unknown>[] = [];
+    if (this.otherCancellables) {
+      this.otherCancellables.forEach(cancellable => {
+        if (cancellable.cancel) {
+          cancelPromises.push(cancellable.cancel());
+        }
+      });
+    }
+
+    while (this.cancelCallbacks.length) {
+      const fn = this.cancelCallbacks.pop();
+      if (fn) {
+        fn();
+      }
+    }
+    return $.when(cancelPromises);
+  }
+
+  onCancel(callback: () => void): CancellableJqPromise<T> {
+    if (this.cancelled) {
+      callback();
+    } else {
+      this.cancelCallbacks.push(callback);
+    }
+    return this;
+  }
+
+  then(then: (result: T) => void): CancellableJqPromise<T> {
+    this.deferred.then(then);
+    return this;
+  }
+
+  done(done: (result: T) => void): CancellableJqPromise<T> {
+    this.deferred.done(done);
+    return this;
+  }
+
+  fail(fail: (error: unknown) => void): CancellableJqPromise<T> {
+    this.deferred.fail(fail);
+    return this;
+  }
+
+  always(always: (result: T) => void): CancellableJqPromise<T> {
+    this.deferred.always(always);
+    return this;
+  }
+
+  pipe(pipe: (result: T) => void): CancellableJqPromise<T> {
+    this.deferred.pipe(pipe);
+    return this;
+  }
+
+  progress(progress: (progress: unknown) => void): CancellableJqPromise<T> {
+    this.deferred.progress(progress);
+    return this;
+  }
+
+  state(): string {
+    return this.deferred.state && this.deferred.state();
+  }
+}

+ 18 - 101
desktop/core/src/desktop/js/api/cancellablePromise.ts

@@ -14,113 +14,30 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import $ from 'jquery';
-import { cancelActiveRequest } from './apiUtils';
+export interface Cancellable {
+  cancel(): void;
+}
 
-export default class CancellablePromise<T> {
-  cancelCallbacks: (() => void)[] = [];
-  deferred: JQuery.Deferred<T>;
-  request?: JQuery.jqXHR;
-  otherCancellables?: CancellablePromise<unknown>[];
-  cancelled = false;
-  cancelPrevented = false;
+export class CancellablePromise<T> extends Promise<T> implements Cancellable {
+  private readonly onCancel?: () => void;
 
   constructor(
-    deferred: JQuery.Deferred<T>,
-    request?: JQuery.jqXHR,
-    otherCancellables?: CancellablePromise<unknown>[]
+    handlers: (
+      resolve: (value?: T | PromiseLike<T>) => void,
+      reject: (reason?: unknown) => void,
+      onCancel?: (cancelHandler: () => void) => void
+    ) => void
   ) {
-    this.deferred = deferred;
-    this.request = request;
-    this.otherCancellables = otherCancellables;
-  }
-
-  /**
-   * A promise might be shared across multiple components in the UI, in some cases cancel is not an option and calling
-   * this will prevent that to happen.
-   *
-   * One example is autocompletion of databases while the assist is loading the database tree, closing the autocomplete
-   * results would make the assist loading fail if cancel hasn't been prevented.
-   *
-   * @returns {CancellablePromise}
-   */
-  preventCancel(): CancellablePromise<T> {
-    this.cancelPrevented = true;
-    return this;
-  }
-
-  cancel(): JQuery.Promise<unknown> {
-    if (this.cancelPrevented || this.cancelled || this.state() !== 'pending') {
-      return $.Deferred().resolve().promise();
-    }
-
-    this.cancelled = true;
-    if (this.request) {
-      cancelActiveRequest(this.request);
-    }
-
-    if (this.state && this.state() === 'pending' && this.deferred.reject) {
-      this.deferred.reject();
-    }
-
-    const cancelPromises: JQuery.Promise<unknown>[] = [];
-    if (this.otherCancellables) {
-      this.otherCancellables.forEach(cancellable => {
-        if (cancellable.cancel) {
-          cancelPromises.push(cancellable.cancel());
-        }
-      });
-    }
-
-    while (this.cancelCallbacks.length) {
-      const fn = this.cancelCallbacks.pop();
-      if (fn) {
-        fn();
-      }
-    }
-    return $.when(cancelPromises);
+    let onCancel: undefined | (() => void) = undefined;
+    super((resolve, reject) =>
+      handlers(resolve, reject, (cancelHandler: () => void) => (onCancel = cancelHandler))
+    );
+    this.onCancel = onCancel;
   }
 
-  onCancel(callback: () => void): CancellablePromise<T> {
-    if (this.cancelled) {
-      callback();
-    } else {
-      this.cancelCallbacks.push(callback);
+  cancel(): void {
+    if (this.onCancel) {
+      this.onCancel();
     }
-    return this;
-  }
-
-  then(then: (result: T) => void): CancellablePromise<T> {
-    this.deferred.then(then);
-    return this;
-  }
-
-  done(done: (result: T) => void): CancellablePromise<T> {
-    this.deferred.done(done);
-    return this;
-  }
-
-  fail(fail: (error: unknown) => void): CancellablePromise<T> {
-    this.deferred.fail(fail);
-    return this;
-  }
-
-  always(always: (result: T) => void): CancellablePromise<T> {
-    this.deferred.always(always);
-    return this;
-  }
-
-  pipe(pipe: (result: T) => void): CancellablePromise<T> {
-    this.deferred.pipe(pipe);
-    return this;
-  }
-
-  progress(progress: (progress: unknown) => void): CancellablePromise<T> {
-    this.deferred.progress(progress);
-    return this;
-  }
-
-  state(): string {
-    return this.deferred.state && this.deferred.state();
   }
 }

+ 9 - 0
desktop/core/src/desktop/js/api/urls.js

@@ -40,6 +40,15 @@ export const TOPO_URL = '/desktop/topo/';
 export const SEARCH_API = '/desktop/api/search/entities';
 export const INTERACTIVE_SEARCH_API = '/desktop/api/search/entities_interactive';
 
+export const CREATE_SESSION_API = '/notebook/api/create_session';
+export const CLOSE_SESSION_API = '/notebook/api/close_session';
+export const FETCH_RESULT_SIZE_API = '/notebook/api/fetch_result_size';
+export const FETCH_RESULT_DATA_API = '/notebook/api/fetch_result_data';
+export const GET_LOGS_API = '/notebook/api/get_logs';
+export const CANCEL_STATEMENT_API = '/notebook/api/cancel_statement';
+export const CLOSE_STATEMENT_API = '/notebook/api/close_statement';
+export const CHECK_STATUS_API = '/notebook/api/check_status';
+
 export const HBASE_API_PREFIX = '/hbase/api/';
 export const SAVE_TO_FILE_API = '/filebrowser/save';
 

+ 383 - 0
desktop/core/src/desktop/js/apps/notebook2/execution/apiUtils.ts

@@ -0,0 +1,383 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { extractErrorMessage, simplePost, successResponseIsError } from 'api/apiUtilsV2';
+import {
+  CANCEL_STATEMENT_API,
+  CHECK_STATUS_API,
+  CLOSE_SESSION_API,
+  CLOSE_STATEMENT_API,
+  CREATE_SESSION_API,
+  EXECUTE_API_PREFIX,
+  FETCH_RESULT_DATA_API,
+  FETCH_RESULT_SIZE_API,
+  GET_LOGS_API
+} from 'api/urls';
+import Executable, {
+  ExecutableContext,
+  EXECUTION_STATUS
+} from 'apps/notebook2/execution/executable';
+
+type SessionPropertyValue = string | number | boolean | null | undefined;
+
+export interface SessionProperty {
+  defaultValue: SessionPropertyValue[];
+  help_text?: string;
+  key: string;
+  multiple?: boolean;
+  nice_name?: string;
+  options?: SessionPropertyValue[];
+  type?: string;
+  value: SessionPropertyValue[];
+}
+
+export interface Session {
+  configuration?: { [key: string]: string };
+  http_addr?: string;
+  id: number;
+  properties: SessionProperty[];
+  reuse_session?: boolean;
+  session_id: string;
+  type: string;
+}
+
+export interface ResultMeta {
+  name: string;
+  type: string;
+  comment?: string | null;
+}
+
+export interface ResultApiResponse {
+  data: (number | string)[][];
+  has_more: boolean;
+  isEscaped: boolean;
+  meta: ResultMeta[];
+  type: string;
+}
+
+export interface ResultSizeApiResponse {
+  rows: number | null;
+  size: number | null;
+  message?: string;
+}
+
+interface FetchResultData extends ExecutableContext {
+  rows: number;
+  startOver?: boolean;
+}
+
+interface FetchLogsData extends ExecutableContext {
+  full_log: string;
+  jobs?: string;
+  from: number;
+}
+
+interface ExecuteData extends ExecutableContext {
+  executable: string;
+}
+
+export interface ExecuteLogsApiResponse {
+  logs: string;
+  isFullLogs: boolean;
+  jobs: ExecutionJob[];
+}
+
+export interface ExecutionJob {
+  percentJob?: number;
+}
+
+export interface ExecutionHandle {
+  end: { row: number; col: number };
+  guid: string;
+  has_more_statements: boolean;
+  has_result_set: boolean;
+  log_context?: unknown;
+  modified_row_count?: number;
+  operation_type: number;
+  previous_statement_hash?: string;
+  secret: string;
+  session_guid: string;
+  session_id: number;
+  session_type: string;
+  start: { row: number; col: number };
+  statement: string;
+  statement_id: number;
+  statements_count: number;
+  sync?: boolean;
+  result?: ResultApiResponse;
+}
+
+export interface ExecutionHistory {
+  id: number;
+  uuid?: string;
+  parentUuid?: string;
+}
+
+export interface ExecuteApiResponse {
+  handle: ExecutionHandle;
+  history?: ExecutionHistory;
+}
+
+export interface ExecuteStatusApiResponse {
+  result?: ResultApiResponse; // For streaming
+  status: string;
+  message?: string;
+}
+
+export interface ExecuteApiOptions {
+  executable: Executable;
+  silenceErrors?: boolean;
+}
+
+export interface AuthRequest {
+  auth: boolean;
+  message?: string;
+}
+
+export const createSession = async (options: {
+  type: string;
+  properties?: SessionProperty[];
+  silenceErrors?: boolean;
+}): Promise<AuthRequest | Session> => {
+  const data = {
+    session: JSON.stringify({ type: options.type, properties: options.properties || [] })
+  };
+
+  const responsePromise = simplePost<
+    {
+      session?: Session;
+      status: number;
+      message?: string;
+    },
+    { session: string }
+  >(CREATE_SESSION_API, data, {
+    silenceErrors: !!options.silenceErrors,
+    ignoreSuccessErrors: true
+  });
+
+  const response = await responsePromise;
+  if (response.status === 401) {
+    return { auth: true, message: response.message };
+  }
+  if (successResponseIsError(response)) {
+    throw new Error(extractErrorMessage(response));
+  }
+
+  if (!response.session) {
+    throw new Error('No session returned.');
+  }
+  return response.session;
+};
+
+export const closeSession = async (options: {
+  session: Session;
+  silenceErrors?: boolean;
+}): Promise<void> => {
+  const data = {
+    session: JSON.stringify(options.session)
+  };
+
+  await simplePost<void, { session: string }>(CLOSE_SESSION_API, data, {
+    silenceErrors: !!options.silenceErrors
+  });
+};
+
+export const executeStatement = async (options: ExecuteApiOptions): Promise<ExecuteApiResponse> => {
+  const executable = options.executable;
+  const url = EXECUTE_API_PREFIX + executable.executor.connector().dialect;
+
+  const data = (await executable.toContext()) as ExecuteData;
+  data.executable = executable.toJson();
+
+  const executePromise = simplePost<
+    {
+      handle: ExecutionHandle;
+      history_id?: number;
+      history_uuid?: string;
+      history_parent_uuid?: string;
+      result?: ResultApiResponse;
+    },
+    ExecuteData
+  >(url, data, {
+    silenceErrors: !!options.silenceErrors
+  });
+
+  executable.addCancellable({
+    cancel: async () => {
+      try {
+        const response = await executePromise;
+        if (options.executable.handle !== response.handle) {
+          options.executable.handle = response.handle;
+        }
+        await cancelStatement(options);
+      } catch (err) {
+        console.warn('Failed cancelling statement');
+        console.warn(err);
+      }
+    }
+  });
+
+  const response = await executePromise;
+
+  if (!response.handle) {
+    throw new Error('No handle in execute response');
+  }
+
+  response.handle.result = response.result;
+
+  const cleanedResponse: ExecuteApiResponse = {
+    handle: response.handle
+  };
+
+  if (typeof response.history_id !== 'undefined') {
+    cleanedResponse.history = {
+      id: response.history_id,
+      uuid: response.history_uuid,
+      parentUuid: response.history_parent_uuid
+    };
+  }
+
+  return cleanedResponse;
+};
+
+export const cancelStatement = async (options: ExecuteApiOptions): Promise<void> => {
+  const data = await options.executable.toContext();
+
+  await simplePost<void, ExecutableContext>(CANCEL_STATEMENT_API, data, {
+    silenceErrors: !!options.silenceErrors
+  });
+};
+
+export const closeStatement = async (options: ExecuteApiOptions): Promise<void> => {
+  if (!options.executable.operationId) {
+    return;
+  }
+  const data = { operationId: options.executable.operationId };
+  await simplePost<void, { operationId: string }>(CLOSE_STATEMENT_API, data, {
+    silenceErrors: !!options.silenceErrors
+  });
+};
+
+export const checkExecutionStatus = async (
+  options: ExecuteApiOptions
+): Promise<ExecuteStatusApiResponse> => {
+  if (!options.executable.operationId) {
+    throw new Error('No operationId given.');
+  }
+
+  const data = { operationId: options.executable.operationId };
+  const responsePromise = simplePost<
+    {
+      query_status?: ExecuteStatusApiResponse;
+      status: number;
+      message?: string;
+    },
+    { operationId: string }
+  >(CHECK_STATUS_API, data, {
+    silenceErrors: !!options.silenceErrors
+  });
+
+  options.executable.addCancellable(responsePromise);
+
+  const response = await responsePromise;
+
+  if (response.query_status) {
+    return response.query_status;
+  }
+
+  if (response.status === -3) {
+    return { status: EXECUTION_STATUS.expired };
+  }
+
+  return { status: EXECUTION_STATUS.failed, message: response.message };
+};
+
+export const fetchResults = async (options: {
+  executable: Executable;
+  rows: number;
+  startOver?: boolean;
+  silenceErrors?: boolean;
+}): Promise<ResultApiResponse> => {
+  const data = (await options.executable.toContext()) as FetchResultData;
+  data.rows = options.rows;
+  data.startOver = options.startOver;
+
+  const responsePromise = simplePost<string, FetchResultData>(FETCH_RESULT_DATA_API, data, {
+    silenceErrors: !!options.silenceErrors,
+    dataType: 'text'
+  });
+
+  options.executable.addCancellable(responsePromise);
+  const response = await (responsePromise as Promise<string>);
+  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+  // @ts-ignore
+  const parsed = JSON.bigdataParse(response);
+  return parsed.result as ResultApiResponse;
+};
+
+export const fetchResultSize = async (
+  options: ExecuteApiOptions
+): Promise<ResultSizeApiResponse> => {
+  const data = await options.executable.toContext();
+
+  const responsePromise = simplePost<
+    {
+      result: ResultSizeApiResponse;
+    },
+    ExecutableContext
+  >(FETCH_RESULT_SIZE_API, data, {
+    silenceErrors: !!options.silenceErrors
+  });
+
+  options.executable.addCancellable(responsePromise);
+  const response = await responsePromise;
+  return response.result;
+};
+
+export const fetchLogs = async (options: {
+  executable: Executable;
+  silenceErrors?: boolean;
+  fullLog: string;
+  jobs?: ExecutionJob[];
+  from?: number;
+}): Promise<ExecuteLogsApiResponse> => {
+  const data = (await options.executable.toContext()) as FetchLogsData;
+  data.full_log = options.fullLog;
+  data.jobs = options.jobs && JSON.stringify(options.jobs);
+  data.from = options.from || 0;
+
+  const responsePromise = simplePost<
+    {
+      logs: string;
+      isFullLogs: boolean;
+      jobs: ExecutionJob[];
+      progress: number;
+      status: number;
+      message?: string;
+    },
+    FetchLogsData
+  >(GET_LOGS_API, data, {
+    silenceErrors: !!options.silenceErrors
+  });
+
+  options.executable.addCancellable(responsePromise);
+  const response = await responsePromise;
+  return {
+    logs: (response.status === 1 && response.message) || response.logs || '',
+    jobs: response.jobs || [],
+    isFullLogs: response.isFullLogs
+  };
+};

+ 199 - 174
desktop/core/src/desktop/js/apps/notebook2/execution/executable.js → desktop/core/src/desktop/js/apps/notebook2/execution/executable.ts

@@ -13,15 +13,24 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
+
 import $ from 'jquery';
 
-import apiHelper from 'api/apiHelper';
+import { Cancellable } from 'api/cancellablePromise';
+import {
+  checkExecutionStatus,
+  closeStatement,
+  ExecuteApiResponse,
+  ExecutionHandle,
+  ExecutionHistory
+} from 'apps/notebook2/execution/apiUtils';
 import ExecutionResult from 'apps/notebook2/execution/executionResult';
 import hueAnalytics from 'utils/hueAnalytics';
 import huePubSub from 'utils/huePubSub';
 import sessionManager from 'apps/notebook2/execution/sessionManager';
-import ExecutionLogs from 'apps/notebook2/execution/executionLogs';
+import ExecutionLogs, { ExecutionLogsRaw } from 'apps/notebook2/execution/executionLogs';
 import hueUtils, { UUID } from 'utils/hueUtils';
+import Executor from 'apps/notebook2/execution/executor';
 
 /**
  *
@@ -45,43 +54,74 @@ export const EXECUTION_STATUS = {
 export const EXECUTABLE_UPDATED_EVENT = 'hue.executable.updated';
 export const EXECUTABLE_STATUS_TRANSITION_EVENT = 'hue.executable.status.transitioned';
 
-const ERROR_REGEX = /line ([0-9]+)(\:([0-9]+))?/i;
-
-export default class Executable {
-  /**
-   * @param options
-   * @param {string} [options.statement] - Either supply a statement or a parsedStatement
-   * @param {Executor} options.executor
-   * @param {Session[]} [options.sessions]
-   */
-  constructor(options) {
-    this.id = UUID();
-    this.executor = options.executor;
-    this.handle = {
-      statement_id: 0 // TODO: Get rid of need for initial handle in the backend
-    };
-    this.operationId = undefined;
-    this.history = undefined;
-    this.status = EXECUTION_STATUS.ready;
-    this.progress = 0;
-    this.result = undefined;
-    this.logs = new ExecutionLogs(this);
-
-    this.cancellables = [];
-    this.notifyThrottle = -1;
+export interface ExecutableRaw {
+  executeEnded: number;
+  executeStarted: number;
+  handle: ExecutionHandle;
+  history?: ExecutionHistory;
+  id: string;
+  logs: ExecutionLogsRaw;
+  lost: boolean;
+  observerState: { [key: string]: unknown };
+  progress: number;
+  status: string;
+  type: string;
+}
 
-    this.executeStarted = 0;
-    this.executeEnded = 0;
+interface hueWindow {
+  WS_CHANNEL?: string;
+  WEB_SOCKETS_ENABLED?: boolean;
+}
 
-    this.previousExecutable = undefined;
-    this.nextExecutable = undefined;
+const INITIAL_HANDLE: ExecutionHandle = {
+  end: { col: 0, row: 0 },
+  guid: '',
+  has_more_statements: false,
+  has_result_set: false,
+  log_context: undefined,
+  modified_row_count: 0,
+  operation_type: 0,
+  previous_statement_hash: '',
+  result: undefined,
+  secret: '',
+  session_guid: '',
+  session_id: 0,
+  session_type: '',
+  start: { col: 0, row: 0 },
+  statement: '',
+  statements_count: 0,
+  sync: false,
+  statement_id: 0
+};
 
-    this.observerState = {};
+export default abstract class Executable {
+  id: string = UUID();
+  database?: string;
+  executor: Executor;
+  handle: ExecutionHandle;
+  operationId?: string;
+  history?: ExecutionHistory;
+  status = EXECUTION_STATUS.ready;
+  progress = 0;
+  result?: ExecutionResult;
+  logs: ExecutionLogs;
+  cancellables: Cancellable[] = [];
+  notifyThrottle = -1;
+  executeStarted = 0;
+  executeEnded = 0;
+  previousExecutable?: Executable;
+  nextExecutable?: Executable;
+  observerState: { [key: string]: unknown } = {};
+  lost = false;
+
+  protected constructor(options: { executor: Executor }) {
+    this.executor = options.executor;
 
-    this.lost = false;
+    this.handle = INITIAL_HANDLE;
+    this.logs = new ExecutionLogs(this);
   }
 
-  setStatus(status) {
+  setStatus(status: string): void {
     const oldStatus = this.status;
     this.status = status;
     if (oldStatus !== status) {
@@ -94,16 +134,16 @@ export default class Executable {
     this.notify();
   }
 
-  setProgress(progress) {
+  setProgress(progress: number): void {
     this.progress = progress;
     this.notify();
   }
 
-  getExecutionTime() {
+  getExecutionTime(): number {
     return (this.executeEnded || Date.now()) - this.executeStarted;
   }
 
-  notify(sync) {
+  notify(sync?: boolean): void {
     window.clearTimeout(this.notifyThrottle);
     if (sync) {
       huePubSub.publish(EXECUTABLE_UPDATED_EVENT, this);
@@ -114,7 +154,7 @@ export default class Executable {
     }
   }
 
-  isReady() {
+  isReady(): boolean {
     return (
       this.status === EXECUTION_STATUS.ready ||
       this.status === EXECUTION_STATUS.closed ||
@@ -122,32 +162,31 @@ export default class Executable {
     );
   }
 
-  isRunning() {
+  isRunning(): boolean {
     return this.status === EXECUTION_STATUS.running || this.status === EXECUTION_STATUS.streaming;
   }
 
-  isSuccess() {
+  isSuccess(): boolean {
     return this.status === EXECUTION_STATUS.success || this.status === EXECUTION_STATUS.available;
   }
 
-  isFailed() {
+  isFailed(): boolean {
     return this.status === EXECUTION_STATUS.failed;
   }
 
-  isPartOfRunningExecution() {
-    if (!this.isReady()) {
-      return true;
-    }
-    return this.previousExecutable && this.previousExecutable.isPartOfRunningExecution();
+  isPartOfRunningExecution(): boolean {
+    return (
+      !this.isReady() ||
+      (!!this.previousExecutable && this.previousExecutable.isPartOfRunningExecution())
+    );
   }
 
-  async cancelBatchChain(wait) {
+  async cancelBatchChain(wait?: boolean): Promise<void> {
     if (this.previousExecutable) {
       this.previousExecutable.nextExecutable = undefined;
+      const cancelPromise = this.previousExecutable.cancelBatchChain(wait);
       if (wait) {
-        await this.previousExecutable.cancelBatchChain(wait);
-      } else {
-        this.previousExecutable.cancelBatchChain();
+        await cancelPromise;
       }
       this.previousExecutable = undefined;
     }
@@ -162,17 +201,16 @@ export default class Executable {
 
     if (this.nextExecutable) {
       this.nextExecutable.previousExecutable = undefined;
+      const cancelPromise = this.nextExecutable.cancelBatchChain(wait);
       if (wait) {
-        await this.nextExecutable.cancelBatchChain(wait);
-      } else {
-        this.nextExecutable.cancelBatchChain();
+        await cancelPromise;
       }
       this.nextExecutable = undefined;
     }
     this.notify();
   }
 
-  async execute() {
+  async execute(): Promise<void> {
     if (!this.isReady()) {
       return;
     }
@@ -191,7 +229,9 @@ export default class Executable {
         const response = await this.internalExecute();
         this.handle = response.handle;
         this.history = response.history;
-        this.operationId = response.history.uuid;
+        if (response.history) {
+          this.operationId = response.history.uuid;
+        }
         if (response.handle.session_id) {
           sessionManager.updateSession({
             type: response.handle.session_type,
@@ -201,27 +241,10 @@ export default class Executable {
           });
         }
       } catch (err) {
-        const match = ERROR_REGEX.exec(err);
-        if (match) {
-          const errorLine = parseInt(match[1]) + this.parsedStatement.location.first_line - 1;
-          let errorCol = match[3] && parseInt(match[3]);
-          if (errorCol && errorLine === 1) {
-            errorCol += this.parsedStatement.location.first_column;
-          }
-
-          const adjustedErr = err.replace(
-            match[0],
-            'line ' + errorLine + (errorCol !== null ? ':' + errorCol : '')
-          );
-
-          this.logs.errors.push(adjustedErr);
-          this.logs.notify();
-
-          throw new Error(adjustedErr);
+        if (typeof err === 'string') {
+          err = this.adaptError(err);
         }
-
         this.logs.errors.push(err);
-
         throw err;
       }
 
@@ -235,7 +258,7 @@ export default class Executable {
         }
       }
 
-      if (this.executor.isOptimizerEnabled) {
+      if (this.executor.isOptimizerEnabled && this.history) {
         huePubSub.publish('editor.upload.query', this.history.id);
       }
 
@@ -246,100 +269,103 @@ export default class Executable {
     }
   }
 
-  checkStatus(statusCheckCount) {
+  async checkStatus(statusCheckCount?: number): Promise<void> {
     let checkStatusTimeout = -1;
 
+    let actualCheckCount = statusCheckCount || 0;
     if (!statusCheckCount) {
-      statusCheckCount = 0;
       this.cancellables.push({
         cancel: () => {
           window.clearTimeout(checkStatusTimeout);
         }
       });
     }
-    statusCheckCount++;
-
-    this.cancellables.push(
-      apiHelper.checkExecutionStatus({ executable: this }).done(async queryStatus => {
-        switch (queryStatus.status) {
-          case EXECUTION_STATUS.success:
-            this.executeEnded = Date.now();
-            this.setStatus(queryStatus.status);
-            this.setProgress(99); // TODO: why 99 here (from old code)?
-            break;
-          case EXECUTION_STATUS.available:
-            this.executeEnded = Date.now();
-            this.setStatus(queryStatus.status);
-            this.setProgress(100);
-            if (!this.result && this.handle.has_result_set) {
-              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.canceled:
-          case EXECUTION_STATUS.expired:
-            this.executeEnded = Date.now();
-            this.setStatus(queryStatus.status);
-            break;
-          case EXECUTION_STATUS.streaming:
-            if (window.WEB_SOCKETS_ENABLED) {
-              huePubSub.publish('editor.ws.query.fetch_result', queryStatus.result);
-            } else {
-              if (!this.result) {
-                this.result = new ExecutionResult(this, true);
-              }
-              this.result.handleResultResponse(queryStatus.result);
-            }
-          case EXECUTION_STATUS.running:
-          case EXECUTION_STATUS.starting:
-          case EXECUTION_STATUS.waiting:
-            this.setStatus(queryStatus.status);
-            checkStatusTimeout = window.setTimeout(
-              () => {
-                this.checkStatus(statusCheckCount);
-              },
-              statusCheckCount > 45 ? 5000 : 1000
-            );
-            break;
-          case EXECUTION_STATUS.failed:
-            this.executeEnded = Date.now();
-            this.setStatus(queryStatus.status);
-            if (queryStatus.message) {
-              $.jHueNotify.error(queryStatus.message); // TODO: Inline instead of popup, e.g. ERROR_REGEX in Execute()
-            }
-            break;
-          default:
-            this.executeEnded = Date.now();
-            this.setStatus(EXECUTION_STATUS.failed);
-            console.warn('Got unknown status ' + queryStatus.status);
+    actualCheckCount++;
+
+    const queryStatus = await checkExecutionStatus({ executable: this });
+
+    switch (queryStatus.status) {
+      case EXECUTION_STATUS.success:
+        this.executeEnded = Date.now();
+        this.setStatus(queryStatus.status);
+        this.setProgress(99); // TODO: why 99 here (from old code)?
+        break;
+      case EXECUTION_STATUS.available:
+        this.executeEnded = Date.now();
+        this.setStatus(queryStatus.status);
+        this.setProgress(100);
+        if (!this.result && this.handle.has_result_set) {
+          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.canceled:
+      case EXECUTION_STATUS.expired:
+        this.executeEnded = Date.now();
+        this.setStatus(queryStatus.status);
+        break;
+      case EXECUTION_STATUS.streaming:
+        if (!queryStatus.result) {
+          return;
+        }
+        if ((<hueWindow>window).WEB_SOCKETS_ENABLED) {
+          huePubSub.publish('editor.ws.query.fetch_result', queryStatus.result);
+        } else {
+          if (!this.result) {
+            this.result = new ExecutionResult(this, true);
+          }
+          this.result.handleResultResponse(queryStatus.result);
+        }
+      case EXECUTION_STATUS.running:
+      case EXECUTION_STATUS.starting:
+      case EXECUTION_STATUS.waiting:
+        this.setStatus(queryStatus.status);
+        checkStatusTimeout = window.setTimeout(
+          () => {
+            this.checkStatus(statusCheckCount);
+          },
+          actualCheckCount > 45 ? 5000 : 1000
+        );
+        break;
+      case EXECUTION_STATUS.failed:
+        this.executeEnded = Date.now();
+        this.setStatus(queryStatus.status);
+        if (queryStatus.message) {
+          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+          // @ts-ignore
+          $.jHueNotify.error(queryStatus.message); // TODO: Inline instead of popup, e.g. ERROR_REGEX in Execute()
+        }
+        break;
+      default:
+        this.executeEnded = Date.now();
+        this.setStatus(EXECUTION_STATUS.failed);
+        console.warn('Got unknown status ' + queryStatus.status);
+    }
   }
 
-  addCancellable(cancellable) {
+  addCancellable(cancellable: Cancellable): void {
     this.cancellables.push(cancellable);
   }
 
-  async internalExecute() {
-    throw new Error('Implement in subclass!');
-  }
+  abstract async internalExecute(): Promise<ExecuteApiResponse>;
 
-  canExecuteInBatch() {
-    throw new Error('Implement in subclass!');
-  }
+  abstract adaptError(err: string): string;
 
-  getKey() {
-    throw new Error('Implement in subclass!');
-  }
+  abstract canExecuteInBatch(): boolean;
 
-  async cancel() {
+  abstract getKey(): string;
+
+  abstract getStatement(): string;
+
+  abstract toJson(): string;
+
+  async cancel(): Promise<void> {
     if (
       this.cancellables.length &&
       (this.status === EXECUTION_STATUS.running || this.status === EXECUTION_STATUS.streaming)
@@ -350,13 +376,16 @@ export default class Executable {
       );
       this.setStatus(EXECUTION_STATUS.canceling);
       while (this.cancellables.length) {
-        await this.cancellables.pop().cancel();
+        const cancellable = this.cancellables.pop();
+        if (cancellable) {
+          await cancellable.cancel();
+        }
       }
       this.setStatus(EXECUTION_STATUS.canceled);
     }
   }
 
-  async reset() {
+  async reset(): Promise<void> {
     this.result = undefined;
     this.logs.reset();
     if (!this.isReady()) {
@@ -364,14 +393,12 @@ export default class Executable {
         await this.close();
       } catch (err) {}
     }
-    this.handle = {
-      statement_id: 0
-    };
+    this.handle = INITIAL_HANDLE;
     this.setProgress(0);
     this.setStatus(EXECUTION_STATUS.ready);
   }
 
-  toJs() {
+  toJs(): ExecutableRaw {
     const state = Object.assign({}, this.observerState);
     delete state.aceAnchor;
 
@@ -390,34 +417,27 @@ export default class Executable {
     };
   }
 
-  async close() {
+  async close(): Promise<void> {
     while (this.cancellables.length) {
       const nextCancellable = this.cancellables.pop();
-      try {
-        await nextCancellable.cancel();
-      } catch (err) {
-        console.warn(err);
+      if (nextCancellable) {
+        try {
+          await nextCancellable.cancel();
+        } catch (err) {
+          console.warn(err);
+        }
       }
     }
 
     try {
-      await apiHelper.closeStatement({ executable: this, silenceErrors: true });
+      await closeStatement({ executable: this, silenceErrors: true });
     } catch (err) {
       console.warn('Failed closing statement');
     }
     this.setStatus(EXECUTION_STATUS.closed);
   }
 
-  async toContext(id) {
-    if (this.snippet) {
-      // V1
-      return {
-        operationId: this.operationId,
-        snippet: this.snippet.toContextJson(),
-        notebook: await this.snippet.parentNotebook.toContextJson()
-      };
-    }
-
+  async toContext(id?: string): Promise<ExecutableContext> {
     const session = await sessionManager.getSession({ type: this.executor.connector().id });
     const statement = this.getStatement();
     const snippet = {
@@ -441,12 +461,11 @@ export default class Executable {
     const notebook = {
       type: this.executor.connector().id,
       snippets: [snippet],
-      id: this.notebookId,
       uuid: hueUtils.UUID(),
       name: '',
       isSaved: false,
       sessions: [session],
-      editorWsChannel: window.WS_CHANNEL
+      editorWsChannel: (<hueWindow>window).WS_CHANNEL
     };
 
     return {
@@ -456,3 +475,9 @@ export default class Executable {
     };
   }
 }
+
+export interface ExecutableContext {
+  operationId?: string;
+  snippet: string;
+  notebook: string;
+}

+ 23 - 16
desktop/core/src/desktop/js/apps/notebook2/execution/executionLogs.js → desktop/core/src/desktop/js/apps/notebook2/execution/executionLogs.ts

@@ -14,30 +14,33 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import apiHelper from 'api/apiHelper';
+import { ExecutionJob, fetchLogs } from 'apps/notebook2/execution/apiUtils';
 import huePubSub from 'utils/huePubSub';
-import { EXECUTION_STATUS } from './executable';
+import Executable, { EXECUTION_STATUS } from './executable';
 
 export const LOGS_UPDATED_EVENT = 'hue.executable.logs.updated';
 
+export interface ExecutionLogsRaw {
+  jobs: ExecutionJob[];
+  errors: string[];
+}
+
 export default class ExecutionLogs {
-  /**
-   *
-   * @param {Executable} executable
-   */
-  constructor(executable) {
+  executable: Executable;
+  fullLog = '';
+  logLines = 0;
+  jobs: ExecutionJob[] = [];
+  errors: string[] = [];
+
+  constructor(executable: Executable) {
     this.executable = executable;
-    this.fullLog = '';
-    this.logLines = 0;
-    this.jobs = [];
-    this.errors = [];
   }
 
-  notify() {
+  notify(): void {
     huePubSub.publish(LOGS_UPDATED_EVENT, this);
   }
 
-  reset() {
+  reset(): void {
     this.fullLog = '';
     this.logLines = 0;
     this.jobs = [];
@@ -45,8 +48,12 @@ export default class ExecutionLogs {
     this.notify();
   }
 
-  async fetchLogs(finalFetch) {
-    const logDetails = await apiHelper.fetchLogs(this);
+  async fetchLogs(finalFetch?: boolean): Promise<void> {
+    const logDetails = await fetchLogs({
+      executable: this.executable,
+      fullLog: this.fullLog,
+      jobs: this.jobs
+    });
 
     if (logDetails.logs.indexOf('Unable to locate') === -1 || logDetails.isFullLogs) {
       this.fullLog = logDetails.logs;
@@ -87,7 +94,7 @@ export default class ExecutionLogs {
     }
   }
 
-  toJs() {
+  toJs(): ExecutionLogsRaw {
     return {
       jobs: this.jobs,
       errors: this.errors

+ 58 - 57
desktop/core/src/desktop/js/apps/notebook2/execution/executionResult.js → desktop/core/src/desktop/js/apps/notebook2/execution/executionResult.ts

@@ -14,12 +14,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {
+  fetchResults,
+  fetchResultSize,
+  ResultApiResponse,
+  ResultMeta,
+  ResultSizeApiResponse
+} from 'apps/notebook2/execution/apiUtils';
+import { Observable } from 'knockout';
 import * as ko from 'knockout';
 
-import apiHelper from 'api/apiHelper';
 import huePubSub from 'utils/huePubSub';
 import { sleep } from 'utils/hueUtils';
-import { EXECUTION_STATUS } from './executable';
+import Executable, { EXECUTION_STATUS } from './executable';
 
 export const RESULT_UPDATED_EVENT = 'hue.executable.result.updated';
 
@@ -27,7 +34,7 @@ export const RESULT_TYPE = {
   TABLE: 'table'
 };
 
-const META_TYPE_TO_CSS = {
+const META_TYPE_TO_CSS: { [key: string]: string } = {
   bigint: 'sort-numeric',
   date: 'sort-date',
   datetime: 'sort-date',
@@ -41,7 +48,7 @@ const META_TYPE_TO_CSS = {
   tinyint: 'sort-numeric'
 };
 
-const NUMERIC_TYPES = {
+const NUMERIC_TYPES: { [key: string]: boolean } = {
   bigint: true,
   decimal: true,
   double: true,
@@ -52,71 +59,74 @@ const NUMERIC_TYPES = {
   tinyint: true
 };
 
-const DATE_TIME_TYPES = {
+const DATE_TIME_TYPES: { [key: string]: boolean } = {
   date: true,
   datetime: true,
   timestamp: true
 };
 
-const COMPLEX_TYPES = {
+const COMPLEX_TYPES: { [key: string]: boolean } = {
   array: true,
   map: true,
   struct: true
 };
 
-huePubSub.subscribe('editor.ws.query.fetch_result', executionResult => {
-  if (executionResult.status != 'finalMessage') {
-    const result = new ExecutionResult(null);
-    result.fetchedOnce = true;
-    result.handleResultResponse(executionResult);
-    // eslint-disable-next-line no-undef
-    executionResult.data.forEach(element => $('#wsResult').append('<li>' + element + '</li>'));
-  }
-});
+// huePubSub.subscribe('editor.ws.query.fetch_result', executionResult => {
+//   if (executionResult.status != 'finalMessage') {
+//     const result = new ExecutionResult(null);
+//     result.fetchedOnce = true;
+//     result.handleResultResponse(executionResult);
+//     // eslint-disable-next-line no-undef
+//     executionResult.data.forEach(element => $('#wsResult').append('<li>' + element + '</li>'));
+//   }
+// });
+
+export interface KoEnrichedMeta extends ResultMeta {
+  cssClass: string;
+  checked: Observable<boolean>;
+  originalIndex: number;
+}
 
 export default class ExecutionResult {
-  /**
-   *
-   * @param {Executable} executable
-   * @param {boolean} [streaming] (default false)
-   */
-  constructor(executable, streaming) {
+  executable: Executable;
+  streaming: boolean;
+
+  type?: string;
+  rows: (string | number)[][] = [];
+  meta: ResultMeta[] = [];
+
+  cleanedMeta: ResultMeta[] = [];
+  cleanedDateTimeMeta: ResultMeta[] = [];
+  cleanedStringMeta: ResultMeta[] = [];
+  cleanedNumericMeta: ResultMeta[] = [];
+  koEnrichedMeta: KoEnrichedMeta[] = [];
+  lastRows: (string | number)[][] = [];
+  images = [];
+
+  hasMore = true;
+  isEscaped = false;
+  fetchedOnce = false;
+
+  constructor(executable: Executable, streaming?: boolean) {
     this.executable = executable;
-
-    this.type = RESULT_TYPE.TABLE;
-    this.streaming = streaming;
-    this.rows = [];
-    this.meta = [];
-
-    this.cleanedMeta = [];
-    this.cleanedDateTimeMeta = [];
-    this.cleanedStringMeta = [];
-    this.cleanedNumericMeta = [];
-    this.koEnrichedMeta = [];
-
-    this.lastRows = [];
-    this.images = [];
-    this.type = undefined;
-    this.hasMore = true;
-    this.isEscaped = false;
-    this.fetchedOnce = false;
+    this.streaming = !!streaming;
   }
 
-  async fetchResultSize() {
+  async fetchResultSize(): Promise<ResultSizeApiResponse | undefined> {
     if (this.executable.status === EXECUTION_STATUS.failed) {
       return;
     }
 
     let attempts = 0;
-    const waitForRows = async () => {
+    const waitForRows = async (): Promise<ResultSizeApiResponse> => {
       attempts++;
       if (attempts < 10) {
-        const resultSizeResponse = await apiHelper.fetchResultSize2({
+        const resultSizeResponse = await fetchResultSize({
           executable: this.executable,
           silenceErrors: true
         });
 
-        if (resultSizeResponse.rows !== null) {
+        if (resultSizeResponse.rows) {
           return resultSizeResponse;
         } else {
           await sleep(1000);
@@ -130,26 +140,17 @@ export default class ExecutionResult {
     return await waitForRows();
   }
 
-  /**
-   * Fetches additional rows
-   *
-   * @param {Object} [options]
-   * @param {number} [options.rows]
-   * @param {boolean} [options.startOver]
-   *
-   * @return {Promise}
-   */
-  async fetchRows(options) {
-    const resultResponse = await apiHelper.fetchResults({
+  async fetchRows(options?: { rows?: number; startOver?: boolean }): Promise<void> {
+    const resultResponse = await fetchResults({
       executable: this.executable,
       rows: (options && options.rows) || 100,
-      startOver: options && options.startOver
+      startOver: !!(options && options.startOver)
     });
 
     this.handleResultResponse(resultResponse);
   }
 
-  handleResultResponse(resultResponse) {
+  handleResultResponse(resultResponse: ResultApiResponse): void {
     const initialIndex = this.rows.length;
     resultResponse.data.forEach((row, index) => {
       row.unshift(initialIndex + index + 1);
@@ -192,7 +193,7 @@ export default class ExecutionResult {
     this.notify();
   }
 
-  notify() {
+  notify(): void {
     huePubSub.publish(RESULT_UPDATED_EVENT, this);
   }
 }

+ 0 - 59
desktop/core/src/desktop/js/apps/notebook2/execution/executor.test.js

@@ -1,59 +0,0 @@
-// Licensed to Cloudera, Inc. under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  Cloudera, Inc. licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import $ from 'jquery';
-
-import { EXECUTION_STATUS } from './executable';
-import Executor from './executor';
-import * as ApiUtils from 'api/apiUtils';
-
-describe('executor.js', () => {
-  /**
-   * @param statement
-   * @return {Executor}
-   */
-  const createSubject = statement =>
-    new Executor({
-      compute: { id: 'compute' },
-      namespace: { id: 'namespace' },
-      database: 'default',
-      connector: () => ({ id: 'impala' }),
-      statement: statement,
-      isSqlEngine: true
-    });
-
-  xit('should set the correct status after successful execute', done => {
-    const subject = createSubject('SELECT * FROM customers;');
-
-    const simplePostDeferred = $.Deferred();
-    jest.spyOn(ApiUtils, 'simplePost').mockImplementation(url => {
-      expect(url).toEqual('/notebook/api/execute/impala');
-      return simplePostDeferred;
-    });
-
-    subject
-      .executeNext()
-      .then(() => {
-        expect(subject.status).toEqual(EXECUTION_STATUS.available);
-        done();
-      })
-      .catch(fail);
-
-    expect(subject.status).toEqual(EXECUTION_STATUS.running);
-
-    simplePostDeferred.resolve({ handle: {} });
-  });
-});

+ 34 - 25
desktop/core/src/desktop/js/apps/notebook2/execution/executor.js → desktop/core/src/desktop/js/apps/notebook2/execution/executor.ts

@@ -14,24 +14,35 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import sessionManager from 'apps/notebook2/execution/sessionManager';
-import { syncExecutables } from 'apps/notebook2/execution/utils';
+import Executable, { ExecutableRaw } from 'apps/notebook2/execution/executable';
+import { syncSqlExecutables } from 'apps/notebook2/execution/utils';
+import { StatementDetails } from 'parse/types';
+import { Compute, Connector, Namespace } from 'types/config';
 
-// TODO: Remove, debug var
-window.sessionManager = sessionManager;
+export interface ExecutorRaw {
+  executables: ExecutableRaw[];
+}
+
+export default class Executor {
+  connector: () => Connector;
+  compute: () => Compute;
+  namespace: () => Namespace;
+  database: () => string;
+  defaultLimit?: () => number;
+  isSqlEngine?: boolean;
+  isOptimizerEnabled?: boolean;
+  executables: Executable[] = [];
 
-class Executor {
-  /**
-   * @param options
-   * @param {boolean} [options.isSqlEngine] (default false)
-   * @param {observable<Connector>} options.connector
-   * @param {observable<ContextCompute>} options.compute
-   * @param {observable<ContextNamespace>} options.namespace
-   * @param {string} [options.database]
-   * @param {function} [options.defaultLimit]
-   * @param {boolean} [options.isOptimizerEnabled] - Default false
-   */
-  constructor(options) {
+  constructor(options: {
+    connector: () => Connector;
+    compute: () => Compute;
+    namespace: () => Namespace;
+    database: () => string;
+    defaultLimit?: () => number;
+    isSqlEngine?: boolean;
+    isOptimizerEnabled?: boolean;
+    executables: Executable[];
+  }) {
     this.connector = options.connector;
     this.compute = options.compute;
     this.namespace = options.namespace;
@@ -39,27 +50,27 @@ class Executor {
     this.isSqlEngine = options.isSqlEngine;
     this.isOptimizerEnabled = options.isOptimizerEnabled;
     this.executables = [];
-    this.defaultLimit = options.defaultLimit || (() => {});
+    this.defaultLimit = options.defaultLimit;
   }
 
-  toJs() {
+  toJs(): ExecutorRaw {
     return {
       executables: this.executables.map(executable => executable.toJs())
     };
   }
 
-  cancelAll() {
+  cancelAll(): void {
     this.executables.forEach(existingExecutable => existingExecutable.cancelBatchChain());
   }
 
-  setExecutables(executables) {
+  setExecutables(executables: Executable[]): void {
     this.cancelAll();
     this.executables = executables;
     this.executables.forEach(executable => executable.notify());
   }
 
-  update(statementDetails, beforeExecute, snippet) {
-    const executables = syncExecutables(this, statementDetails, snippet);
+  update(statementDetails: StatementDetails, beforeExecute: boolean): Executable {
+    const executables = syncSqlExecutables(this, statementDetails);
 
     // Cancel any "lost" executables and any batch chain it's part of
     executables.lost.forEach(lostExecutable => {
@@ -71,7 +82,7 @@ class Executor {
     if (beforeExecute) {
       executables.selected.forEach(executable => executable.cancelBatchChain());
 
-      let previous = undefined;
+      let previous: Executable | undefined;
       executables.selected.forEach(executable => {
         if (previous) {
           executable.previousExecutable = previous;
@@ -89,5 +100,3 @@ class Executor {
     return executables.selected[0];
   }
 }
-
-export default Executor;

+ 73 - 30
desktop/core/src/desktop/js/apps/notebook2/execution/sessionManager.test.js → desktop/core/src/desktop/js/apps/notebook2/execution/sessionManager.test.ts

@@ -14,29 +14,49 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import $ from 'jquery';
+import { CancellablePromise } from 'api/cancellablePromise';
+import { CLOSE_SESSION_API } from 'api/urls';
+import { AuthRequest, Session, SessionProperty } from './apiUtils';
 
-import ApiHelper from 'api/apiHelper';
+import * as ApiUtils from './apiUtils';
+import * as ApiUtilsV2 from 'api/apiUtilsV2';
 import sessionManager from './sessionManager';
-import * as ApiUtils from 'api/apiUtils';
 
-describe('sessionManager.js', () => {
-  let spy;
+describe('sessionManager.ts', () => {
+  let spy: jest.SpyInstance<
+    Promise<AuthRequest | Session>,
+    [
+      {
+        type: string;
+        properties?: SessionProperty[] | undefined;
+        silenceErrors?: boolean | undefined;
+      }
+    ]
+  >;
+
   beforeEach(() => {
     // sessionManager is a singleton so we need to clear out sessions between tests
     sessionManager.knownSessionPromises = {};
-    const sessionCount = {};
-    const getSessionCount = type => {
+    const sessionCount: { [key: string]: number } = {};
+    const getSessionCount = (type: string) => {
       if (!sessionCount[type]) {
         sessionCount[type] = 0;
       }
       return sessionCount[type]++;
     };
-    spy = jest.spyOn(ApiHelper, 'createSession').mockImplementation(async sessionDef =>
-      Promise.resolve({
-        session_id: sessionDef.type + '_' + getSessionCount(sessionDef.type),
-        type: sessionDef.type
-      })
+    spy = jest.spyOn(ApiUtils, 'createSession').mockImplementation(
+      async (options: {
+        type: string;
+        properties?: SessionProperty[];
+        silenceErrors?: boolean;
+      }): Promise<Session | AuthRequest> =>
+        Promise.resolve({
+          session_id: options.type + '_' + getSessionCount(options.type),
+          type: options.type,
+          properties: options.properties || [],
+          reuse_session: true,
+          id: 0
+        })
     );
   });
 
@@ -46,20 +66,23 @@ describe('sessionManager.js', () => {
   });
 
   it('should create detached sessions', async () => {
+    const mockProperties: unknown[] = [{ key: 'someKey', value: ['someValue'] }];
     const sessionDetails = {
       type: 'impala',
-      properties: [{ key: 'someKey', value: 'someValue' }]
+      properties: mockProperties as SessionProperty[]
     };
 
     expect((await sessionManager.getAllSessions()).length).toEqual(0);
 
     const session = await sessionManager.createDetachedSession(sessionDetails);
-
     expect(session.session_id).toEqual('impala_0');
+    expect(session.properties.length).toEqual(1);
+    expect(session.properties[0].key).toEqual((mockProperties[0] as SessionProperty).key);
+    expect(session.properties[0].value).toEqual((mockProperties[0] as SessionProperty).value);
 
     expect((await sessionManager.getAllSessions()).length).toEqual(0);
     expect(sessionManager.hasSession('impala')).toBeFalsy();
-    expect(ApiHelper.createSession).toHaveBeenCalledWith(sessionDetails);
+    expect(ApiUtils.createSession).toHaveBeenCalledWith(sessionDetails);
   });
 
   it('should keep one sessions instance per type', async () => {
@@ -75,7 +98,7 @@ describe('sessionManager.js', () => {
 
     expect((await sessionManager.getAllSessions()).length).toEqual(1);
     expect(sessionManager.hasSession('impala')).toBeTruthy();
-    expect(ApiHelper.createSession).toHaveBeenCalledTimes(1);
+    expect(ApiUtils.createSession).toHaveBeenCalledTimes(1);
   });
 
   it('should keep track of multiple instance per type', async () => {
@@ -92,7 +115,7 @@ describe('sessionManager.js', () => {
     expect((await sessionManager.getAllSessions()).length).toEqual(2);
     expect(sessionManager.hasSession('impala')).toBeTruthy();
     expect(sessionManager.hasSession('hive')).toBeTruthy();
-    expect(ApiHelper.createSession).toHaveBeenCalledTimes(2);
+    expect(ApiUtils.createSession).toHaveBeenCalledTimes(2);
   });
 
   it('should stop tracking sessions when closed', async () => {
@@ -104,18 +127,35 @@ describe('sessionManager.js', () => {
     expect(session.session_id).toEqual('impala_0');
     expect(sessionManager.hasSession('impala')).toBeTruthy();
 
+    //(url, data, options) => {
+    //       expect(JSON.parse(data.session).session_id).toEqual(session.session_id);
+    //       expect(options.silenceErrors).toBeTruthy();
+    //       expect(url).toEqual(CLOSE_SESSION_API);
+    //       return new $.Deferred().resolve().promise();
+    //     }
     // Close the session
-    const postSpy = jest.spyOn(ApiUtils, 'simplePost').mockImplementation((url, data, options) => {
-      expect(JSON.parse(data.session).session_id).toEqual(session.session_id);
-      expect(options.silenceErrors).toBeTruthy();
-      expect(url).toEqual('/notebook/api/close_session');
-      return new $.Deferred().resolve().promise();
-    });
+
+    const postSpy = jest.spyOn(ApiUtilsV2, 'simplePost').mockImplementation(
+      (
+        url: string,
+        data: unknown,
+        options?: { dataType?: string; silenceErrors?: boolean; ignoreSuccessErrors?: boolean }
+      ): CancellablePromise<unknown> => {
+        expect(JSON.parse((data as { session: string }).session).session_id).toEqual(
+          session.session_id
+        );
+        expect(options && options.silenceErrors).toBeTruthy();
+        expect(url).toEqual(CLOSE_SESSION_API);
+        return new CancellablePromise(resolve => {
+          resolve();
+        });
+      }
+    );
     await sessionManager.closeSession(session);
 
     expect(sessionManager.hasSession('impala')).toBeFalsy();
-    expect(ApiHelper.createSession).toHaveBeenCalledTimes(1);
-    expect(ApiUtils.simplePost).toHaveBeenCalledTimes(1);
+    expect(ApiUtils.createSession).toHaveBeenCalledTimes(1);
+    expect(ApiUtilsV2.simplePost).toHaveBeenCalledTimes(1);
     postSpy.mockClear();
   });
 
@@ -129,16 +169,19 @@ describe('sessionManager.js', () => {
     expect(sessionManager.hasSession('impala')).toBeTruthy();
 
     // Restart the session
-    const postSpy = jest
-      .spyOn(ApiUtils, 'simplePost')
-      .mockReturnValue(new $.Deferred().resolve().promise());
+    const postSpy = jest.spyOn(ApiUtilsV2, 'simplePost').mockImplementation(
+      (): CancellablePromise<unknown> =>
+        new CancellablePromise(resolve => {
+          resolve();
+        })
+    );
     session = await sessionManager.restartSession(session);
 
     expect(session.session_id).toEqual('impala_1');
     expect(sessionManager.hasSession('impala')).toBeTruthy();
 
-    expect(ApiHelper.createSession).toHaveBeenCalledTimes(2);
-    expect(ApiUtils.simplePost).toHaveBeenCalledTimes(1);
+    expect(ApiUtils.createSession).toHaveBeenCalledTimes(2);
+    expect(ApiUtilsV2.simplePost).toHaveBeenCalledTimes(1);
     postSpy.mockClear();
   });
 });

+ 41 - 63
desktop/core/src/desktop/js/apps/notebook2/execution/sessionManager.js → desktop/core/src/desktop/js/apps/notebook2/execution/sessionManager.ts

@@ -14,48 +14,25 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import apiHelper from 'api/apiHelper';
+import {
+  AuthRequest,
+  closeSession,
+  createSession,
+  Session,
+  SessionProperty
+} from 'apps/notebook2/execution/apiUtils';
 import huePubSub from 'utils/huePubSub';
 
 class SessionManager {
-  constructor() {
-    this.knownSessionPromises = {};
-  }
-  /**
-   * @typedef SessionProperty
-   * @property {Array<*>} [defaultValue]
-   * @property {String} [help_text]
-   * @property {String} key
-   * @property {Boolean} [multiple]
-   * @property {String} [nice_name]
-   * @property {Array<*>} [options]
-   * @property {String} [type]
-   * @property {Array<*>} value
-   */
-
-  /**
-   * @typedef Session
-   * @property {Object.<string, string>} configuration
-   * @property {string} http_addr
-   * @property {number} id
-   * @property {Array<SessionProperty>} properties
-   * @property {boolean} reuse_session
-   * @property {string} session_id
-   * @property {string} type
-   */
+  knownSessionPromises: { [key: string]: Promise<Session> } = {};
 
   /**
    * Gets an existing session or creates a new one if there is no session.
-   *
-   * @param {Object} options
-   * @param {String} options.type
-   * @param {Array<SessionProperty>} [options.properties] - Default []
-   *
-   * @return {Promise<Session>}
    */
-  async getSession(options) {
+  async getSession(options: { type: string; properties?: SessionProperty[] }): Promise<Session> {
     if (!this.knownSessionPromises[options.type]) {
       this.knownSessionPromises[options.type] = this.createDetachedSession(options);
+
       // Sessions that fail
       this.knownSessionPromises[options.type].catch(() => {
         delete this.knownSessionPromises[options.type];
@@ -74,60 +51,61 @@ class SessionManager {
    *
    * @return {Promise<Session>}
    */
-  async createDetachedSession(options) {
-    return new Promise((resolve, reject) => {
+  async createDetachedSession(options: {
+    type: string;
+    properties?: SessionProperty[];
+    preventAuthModal?: boolean;
+  }): Promise<Session> {
+    return new Promise(async (resolve, reject) => {
       const sessionToCreate = {
         type: options.type,
         properties: options.properties || []
       };
-      apiHelper
-        .createSession(sessionToCreate)
-        .then(resolve)
-        .catch(reason => {
-          if (reason && reason.auth) {
-            // The auth modal will resolve or reject
-            if (!options.preventAuthModal) {
-              huePubSub.publish('show.session.auth.modal', {
-                message: reason.message,
-                session: sessionToCreate,
-                resolve: resolve,
-                reject: reject
-              });
-            }
-          } else {
-            reject(reason);
-          }
-        });
+
+      const sessionOrAuth = await createSession(sessionToCreate);
+      if ('auth' in sessionOrAuth && sessionOrAuth.auth) {
+        const auth = sessionOrAuth as AuthRequest;
+        if (!options.preventAuthModal) {
+          huePubSub.publish('show.session.auth.modal', {
+            message: auth.message,
+            session: sessionToCreate,
+            resolve: resolve,
+            reject: reject
+          });
+        } else {
+          reject(auth);
+        }
+      } else {
+        resolve(sessionOrAuth as Session);
+      }
     });
   }
 
-  updateSession(options) {
-    this.knownSessionPromises[options.type] = new Promise((resolve, reject) => {
-      resolve(options);
-    });
+  updateSession(session: Session) {
+    this.knownSessionPromises[session.type] = Promise.resolve(session);
   }
 
-  async getAllSessions() {
+  async getAllSessions(): Promise<Session[]> {
     const promises = Object.keys(this.knownSessionPromises).map(
       key => this.knownSessionPromises[key]
     );
     return Promise.all(promises);
   }
 
-  async restartSession(session) {
+  async restartSession(session: Session): Promise<Session> {
     await this.closeSession(session);
-    return this.getSession({ type: session.type, properties: session.properties });
+    return this.getSession(session);
   }
 
-  hasSession(type) {
+  hasSession(type: string): boolean {
     return !!this.knownSessionPromises[type];
   }
 
-  async closeSession(session) {
+  async closeSession(session: Session): Promise<void> {
     if (this.hasSession(session.type)) {
       delete this.knownSessionPromises[session.type];
     }
-    await apiHelper.closeSession({ session: session, silenceErrors: true });
+    await closeSession({ session: session, silenceErrors: true });
   }
 }
 

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

@@ -1,155 +0,0 @@
-// Licensed to Cloudera, Inc. under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  Cloudera, Inc. licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import $ from 'jquery';
-
-import ApiHelper from 'api/apiHelper';
-import SqlExecutable from './sqlExecutable';
-import { EXECUTION_STATUS } from './executable';
-import sessionManager from './sessionManager';
-import * as ApiUtils from 'api/apiUtils';
-
-describe('sqlExecutable.js', () => {
-  afterEach(() => {
-    sessionManager.knownSessionPromises = {};
-  });
-
-  /**
-   * @param statement
-   * @return {SqlExecutable}
-   */
-  const createSubject = (statement, limit) => {
-    if (typeof statement === 'string') {
-      return new SqlExecutable({
-        database: 'default',
-        parsedStatement: { statement: statement, firstToken: 'select' },
-        sourceType: 'impala',
-        executor: {
-          defaultLimit: () => limit
-        }
-      });
-    }
-
-    return new SqlExecutable({
-      compute: { id: 'compute' },
-      namespace: { id: 'namespace' },
-      database: 'default',
-      parsedStatement: statement,
-      sourceType: 'impala',
-      executor: {
-        defaultLimit: () => limit
-      }
-    });
-  };
-
-  it('should handle strings statements', () => {
-    const subject = createSubject('SELECT * FROM customers');
-
-    expect(subject.getStatement()).toEqual('SELECT * FROM customers');
-  });
-
-  it('should handle parsed statements', () => {
-    const subject = createSubject({ statement: 'SELECT * FROM customers', firstToken: 'SELECT' });
-
-    expect(subject.getStatement()).toEqual('SELECT * FROM customers');
-  });
-
-  xit('should set the correct status after successful execute', done => {
-    const subject = createSubject('SELECT * FROM customers');
-
-    const simplePostDeferred = $.Deferred();
-    jest.spyOn(ApiUtils, 'simplePost').mockImplementation(url => {
-      expect(url).toEqual('/notebook/api/execute/impala');
-      return simplePostDeferred;
-    });
-
-    subject
-      .execute()
-      .then(() => {
-        expect(subject.status).toEqual(EXECUTION_STATUS.available);
-        done();
-      })
-      .catch(fail);
-
-    expect(subject.status).toEqual(EXECUTION_STATUS.running);
-
-    simplePostDeferred.resolve({ handle: {} });
-  });
-
-  xit('should set the correct status after failed execute', done => {
-    const subject = createSubject('SELECT * FROM customers');
-
-    const simplePostDeferred = $.Deferred();
-
-    jest.spyOn(ApiHelper, 'createSession').mockImplementation(
-      () =>
-        new Promise(resolve => {
-          resolve({ type: 'x' });
-        })
-    );
-
-    jest.spyOn(ApiUtils, 'simplePost').mockImplementation(url => {
-      expect(url).toEqual('/notebook/api/execute/impala');
-      return simplePostDeferred;
-    });
-
-    subject
-      .execute()
-      .then(fail)
-      .catch(() => {
-        expect(subject.status).toEqual(EXECUTION_STATUS.failed);
-        done();
-      });
-
-    expect(subject.status).toEqual(EXECUTION_STATUS.running);
-
-    simplePostDeferred.reject();
-  });
-
-  xit('should set the correct status when cancelling', done => {
-    const subject = createSubject('SELECT * FROM customers');
-
-    const simplePostExeuteDeferred = $.Deferred();
-    const simplePostCancelDeferred = $.Deferred();
-    jest.spyOn(ApiUtils, 'simplePost').mockImplementation(url => {
-      if (url === '/notebook/api/execute/impala') {
-        return simplePostExeuteDeferred;
-      } else if (url === '/notebook/api/cancel_statement') {
-        return simplePostCancelDeferred;
-      }
-      fail();
-    });
-
-    subject
-      .execute()
-      .then(() => {
-        expect(subject.status).toEqual(EXECUTION_STATUS.available);
-        subject.cancel().then(() => {
-          expect(subject.status).toEqual(EXECUTION_STATUS.canceled);
-          done();
-        });
-
-        expect(subject.status).toEqual(EXECUTION_STATUS.canceling);
-
-        simplePostCancelDeferred.resolve();
-      })
-      .catch(fail);
-
-    expect(subject.status).toEqual(EXECUTION_STATUS.running);
-
-    simplePostExeuteDeferred.resolve({ handle: {} });
-  });
-});

+ 213 - 0
desktop/core/src/desktop/js/apps/notebook2/execution/sqlExecutable.test.ts

@@ -0,0 +1,213 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { CancellablePromise } from 'api/cancellablePromise';
+import { CHECK_STATUS_API, CREATE_SESSION_API, EXECUTE_API_PREFIX, GET_LOGS_API } from 'api/urls';
+
+import Executor from 'apps/notebook2/execution/executor';
+import SqlExecutable from './sqlExecutable';
+import { EXECUTION_STATUS } from './executable';
+import sessionManager from './sessionManager';
+import * as ApiUtils from 'api/apiUtilsV2';
+import { ParsedSqlStatement } from 'parse/sqlStatementsParser';
+
+describe('sqlExecutable.js', () => {
+  afterEach(() => {
+    sessionManager.knownSessionPromises = {};
+  });
+
+  const mockExecutor = (mock: unknown): Executor => mock as Executor;
+
+  const createParsedStatement = (statement: string, firstToken: string): ParsedSqlStatement => ({
+    location: { first_line: 0, first_column: 0, last_line: 1, last_column: 0 },
+    firstToken: firstToken,
+    statement: statement,
+    type: 'statement'
+  });
+
+  const createSubject = (
+    statement: string | ParsedSqlStatement,
+    limit?: number,
+    dialect?: string
+  ): SqlExecutable => {
+    if (typeof statement === 'string') {
+      return new SqlExecutable({
+        database: 'default',
+        parsedStatement: createParsedStatement(statement, 'seleft'),
+        executor: mockExecutor({
+          connector: () => ({ id: 'test', dialect: dialect || '' }),
+          compute: () => ({ id: 'test' }),
+          namespace: () => ({ id: 'test' }),
+          defaultLimit: () => limit,
+          toJs: () => ({})
+        })
+      });
+    }
+
+    return new SqlExecutable({
+      database: 'default',
+      parsedStatement: statement,
+      executor: mockExecutor({
+        defaultLimit: () => limit
+      })
+    });
+  };
+
+  const SELECT_STATEMENT = 'SELECT * FROM customers';
+
+  it('should handle strings statements', () => {
+    const subject = createSubject(SELECT_STATEMENT);
+
+    expect(subject.getStatement()).toEqual(SELECT_STATEMENT);
+  });
+
+  it('should handle parsed statements', () => {
+    const subject = createSubject(createParsedStatement(SELECT_STATEMENT, 'SELECT'));
+
+    expect(subject.getStatement()).toEqual(SELECT_STATEMENT);
+  });
+
+  it('should set the correct status after successful execute', async () => {
+    const subject = createSubject(SELECT_STATEMENT, undefined, 'impala');
+
+    let createSessionApiHit = -1;
+    let executeApiHit = -1;
+    let checkStatusApiHit = -1;
+    let getLogsApiHit = -1;
+
+    let currentApiHit = 0;
+
+    let logsResolve: (data: unknown) => void;
+    const logsPromise = new CancellablePromise<unknown>(resolve => {
+      logsResolve = resolve;
+    });
+
+    let statusResolve: (data: unknown) => void;
+    const statusPromise = new CancellablePromise<unknown>(resolve => {
+      statusResolve = resolve;
+    });
+
+    jest.spyOn(ApiUtils, 'simplePost').mockImplementation(
+      (url: string): CancellablePromise<unknown> => {
+        currentApiHit++;
+        if (url.indexOf(CREATE_SESSION_API) !== -1) {
+          createSessionApiHit = currentApiHit;
+          return new CancellablePromise<unknown>(resolve => {
+            resolve({ session: { type: 'foo' } });
+          });
+        } else if (url.indexOf(EXECUTE_API_PREFIX) !== -1) {
+          executeApiHit = currentApiHit;
+          expect(url).toEqual(EXECUTE_API_PREFIX + 'impala');
+          return new CancellablePromise<unknown>(resolve => {
+            resolve({
+              handle: {},
+              history_id: 1,
+              history_uuid: 'some-history_uuid',
+              history_parent_uuid: 'some_history_parent_uuid'
+            });
+          });
+        } else if (url.indexOf(CHECK_STATUS_API) !== -1) {
+          checkStatusApiHit = currentApiHit;
+          statusResolve({ query_status: { status: EXECUTION_STATUS.available } });
+          return statusPromise;
+        } else if (url.indexOf(GET_LOGS_API) !== -1) {
+          getLogsApiHit = currentApiHit;
+          logsResolve({ status: 0, logs: '' });
+          return logsPromise;
+        }
+        fail('fail for URL: ' + url);
+        throw new Error('fail for URL: ' + url);
+      }
+    );
+
+    expect(subject.status).toEqual(EXECUTION_STATUS.ready);
+
+    await subject.execute();
+    await statusPromise;
+    await logsPromise;
+
+    expect(createSessionApiHit).toEqual(1);
+    expect(executeApiHit).toEqual(2);
+    expect(checkStatusApiHit).toEqual(3);
+    expect(getLogsApiHit).toEqual(4);
+
+    expect(subject.status).toEqual(EXECUTION_STATUS.available);
+  });
+
+  // xit('should set the correct status after failed execute', done => {
+  //   const subject = createSubject('SELECT * FROM customers');
+  //
+  //   const simplePostDeferred = $.Deferred();
+  //
+  //   jest.spyOn(ApiHelper, 'createSession').mockImplementation(
+  //     () =>
+  //       new Promise(resolve => {
+  //         resolve({ type: 'x' });
+  //       })
+  //   );
+  //
+  //   jest.spyOn(ApiUtils, 'simplePost').mockImplementation(url => {
+  //     expect(url).toEqual('/notebook/api/execute/impala');
+  //     return simplePostDeferred;
+  //   });
+  //
+  //   subject
+  //     .execute()
+  //     .then(fail)
+  //     .catch(() => {
+  //       expect(subject.status).toEqual(EXECUTION_STATUS.failed);
+  //       done();
+  //     });
+  //
+  //   expect(subject.status).toEqual(EXECUTION_STATUS.running);
+  //
+  //   simplePostDeferred.reject();
+  // });
+
+  // xit('should set the correct status when cancelling', done => {
+  //   const subject = createSubject('SELECT * FROM customers');
+  //
+  //   const simplePostExeuteDeferred = $.Deferred();
+  //   const simplePostCancelDeferred = $.Deferred();
+  //   jest.spyOn(ApiUtils, 'simplePost').mockImplementation(url => {
+  //     if (url === '/notebook/api/execute/impala') {
+  //       return simplePostExeuteDeferred;
+  //     } else if (url === '/notebook/api/cancel_statement') {
+  //       return simplePostCancelDeferred;
+  //     }
+  //     fail();
+  //   });
+  //
+  //   subject
+  //     .execute()
+  //     .then(() => {
+  //       expect(subject.status).toEqual(EXECUTION_STATUS.available);
+  //       subject.cancel().then(() => {
+  //         expect(subject.status).toEqual(EXECUTION_STATUS.canceled);
+  //         done();
+  //       });
+  //
+  //       expect(subject.status).toEqual(EXECUTION_STATUS.canceling);
+  //
+  //       simplePostCancelDeferred.resolve();
+  //     })
+  //     .catch(fail);
+  //
+  //   expect(subject.status).toEqual(EXECUTION_STATUS.running);
+  //
+  //   simplePostExeuteDeferred.resolve({ handle: {} });
+  // });
+});

+ 52 - 24
desktop/core/src/desktop/js/apps/notebook2/execution/sqlExecutable.js → desktop/core/src/desktop/js/apps/notebook2/execution/sqlExecutable.ts

@@ -14,33 +14,41 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import apiHelper from 'api/apiHelper';
-import Executable from 'apps/notebook2/execution/executable';
+import { ExecuteApiResponse, executeStatement } from 'apps/notebook2/execution/apiUtils';
+import Executable, { ExecutableRaw } from 'apps/notebook2/execution/executable';
+import Executor from 'apps/notebook2/execution/executor';
+import { ParsedSqlStatement } from 'parse/sqlStatementsParser';
 
 const BATCHABLE_STATEMENT_TYPES = /ALTER|CREATE|DELETE|DROP|GRANT|INSERT|INVALIDATE|LOAD|SET|TRUNCATE|UPDATE|UPSERT|USE/i;
 
 const SELECT_END_REGEX = /([^;]*)([;]?[^;]*)/;
+const ERROR_REGEX = /line ([0-9]+)(:([0-9]+))?/i;
+
+export interface SqlExecutableRaw extends ExecutableRaw {
+  database: string;
+  parsedStatement: ParsedSqlStatement;
+}
 
 export default class SqlExecutable extends Executable {
-  /**
-   * @param options
-   * @param {Executor} options.executor
-   *
-   * @param {string} options.database
-   * @param {SqlStatementsParserResult} options.parsedStatement
-   */
-  constructor(options) {
+  database: string;
+  parsedStatement: ParsedSqlStatement;
+
+  constructor(options: {
+    executor: Executor;
+    database: string;
+    parsedStatement: ParsedSqlStatement;
+  }) {
     super(options);
     this.database = options.database;
     this.parsedStatement = options.parsedStatement;
   }
 
-  getStatement() {
-    let statement = this.statement || this.parsedStatement.statement;
+  getStatement(): string {
+    let statement = this.parsedStatement.statement;
     if (
-      this.parsedStatement &&
       this.parsedStatement.firstToken &&
       this.parsedStatement.firstToken.toLowerCase() === 'select' &&
+      this.executor.defaultLimit &&
       !isNaN(this.executor.defaultLimit()) &&
       this.executor.defaultLimit() > 0 &&
       !/\slimit\s[0-9]/i.test(statement)
@@ -53,25 +61,26 @@ export default class SqlExecutable extends Executable {
         }
       }
     }
+
     return statement;
   }
 
-  async internalExecute() {
-    return await apiHelper.executeStatement({
+  async internalExecute(): Promise<ExecuteApiResponse> {
+    return await executeStatement({
       executable: this,
       silenceErrors: true
     });
   }
 
-  getKey() {
+  getKey(): string {
     return this.database + '_' + this.parsedStatement.statement;
   }
 
-  canExecuteInBatch() {
+  canExecuteInBatch(): boolean {
     return this.parsedStatement && BATCHABLE_STATEMENT_TYPES.test(this.parsedStatement.firstToken);
   }
 
-  static fromJs(executor, executableRaw) {
+  static fromJs(executor: Executor, executableRaw: SqlExecutableRaw): SqlExecutable {
     const executable = new SqlExecutable({
       database: executableRaw.database,
       executor: executor,
@@ -92,21 +101,40 @@ export default class SqlExecutable extends Executable {
     return executable;
   }
 
-  toJs() {
-    const executableJs = super.toJs();
+  toJs(): SqlExecutableRaw {
+    const executableJs = (super.toJs() as unknown) as SqlExecutableRaw;
     executableJs.database = this.database;
     executableJs.parsedStatement = this.parsedStatement;
     executableJs.type = 'sqlExecutable';
     return executableJs;
   }
 
-  // TODO: Use this for execute instead of snippet
-  toJson() {
+  toJson(): string {
     return JSON.stringify({
-      id: this.id,
       statement: this.getStatement(),
       database: this.database
-      // session:
     });
   }
+
+  adaptError(err: string): string {
+    const match = ERROR_REGEX.exec(err);
+    if (match) {
+      const errorLine = parseInt(match[1]) + this.parsedStatement.location.first_line - 1;
+      let errorCol = match[3] && parseInt(match[3]);
+      if (errorCol && errorLine === 1) {
+        errorCol += this.parsedStatement.location.first_column;
+      }
+
+      const adjustedErr = err.replace(
+        match[0],
+        'line ' + errorLine + (errorCol !== null ? ':' + errorCol : '')
+      );
+
+      this.logs.errors.push(adjustedErr);
+      this.logs.notify();
+
+      return adjustedErr;
+    }
+    return err;
+  }
 }

+ 31 - 23
desktop/core/src/desktop/js/apps/notebook2/execution/utils.test.js → desktop/core/src/desktop/js/apps/notebook2/execution/utils.test.ts

@@ -14,17 +14,21 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { syncExecutables } from 'apps/notebook2/execution/utils';
+import Executor from 'apps/notebook2/execution/executor';
+import { syncSqlExecutables } from 'apps/notebook2/execution/utils';
 import sqlStatementsParser from 'parse/sqlStatementsParser';
 
-describe('utils.js', () => {
+const mockExecutor = (mock: unknown): Executor => mock as Executor;
+
+describe('utils.ts', () => {
   it('create new when no executables present', () => {
-    const executor = {
+    const executor = mockExecutor({
       database: () => 'someDb',
       executables: []
-    };
+    });
     const statements = sqlStatementsParser.parse('SELECT 1;');
-    const result = syncExecutables(executor, {
+
+    const result = syncSqlExecutables(executor, {
       precedingStatements: [],
       activeStatement: statements[0],
       followingStatements: [],
@@ -41,7 +45,7 @@ describe('utils.js', () => {
 
   it('should reuse existing executables when nothing has changed', () => {
     const statements = sqlStatementsParser.parse('SELECT 1;');
-    const executor = {
+    const executor = mockExecutor({
       database: () => 'someDb',
       executables: [
         {
@@ -49,8 +53,8 @@ describe('utils.js', () => {
           parsedStatement: JSON.parse(JSON.stringify(statements[0]))
         }
       ]
-    };
-    const result = syncExecutables(executor, {
+    });
+    const result = syncSqlExecutables(executor, {
       precedingStatements: [],
       activeStatement: statements[0],
       followingStatements: [],
@@ -66,7 +70,7 @@ describe('utils.js', () => {
   });
 
   it('should mark existing executables as edited when the database has changed', () => {
-    const executor = {
+    const executor = mockExecutor({
       database: () => 'someOtherDb',
       executables: [
         {
@@ -74,9 +78,10 @@ describe('utils.js', () => {
           parsedStatement: sqlStatementsParser.parse('SELECT 1;')[0]
         }
       ]
-    };
+    });
+
     const newStatements = sqlStatementsParser.parse('SELECT 2;');
-    const result = syncExecutables(executor, {
+    const result = syncSqlExecutables(executor, {
       precedingStatements: [],
       activeStatement: newStatements[0],
       followingStatements: [],
@@ -93,7 +98,7 @@ describe('utils.js', () => {
   });
 
   it('should mark existing executables as edited when same index and the statement has changed', () => {
-    const executor = {
+    const executor = mockExecutor({
       database: () => 'someDb',
       executables: [
         {
@@ -101,9 +106,10 @@ describe('utils.js', () => {
           parsedStatement: sqlStatementsParser.parse('SELECT 1;')[0]
         }
       ]
-    };
+    });
+
     const newStatements = sqlStatementsParser.parse('SELECT 2;');
-    const result = syncExecutables(executor, {
+    const result = syncSqlExecutables(executor, {
       precedingStatements: [],
       activeStatement: newStatements[0],
       followingStatements: [],
@@ -119,7 +125,7 @@ describe('utils.js', () => {
   });
 
   it('should not mark existing executables as edited when nothing has changed', () => {
-    const executor = {
+    const executor = mockExecutor({
       database: () => 'someDb',
       executables: [
         {
@@ -127,9 +133,10 @@ describe('utils.js', () => {
           parsedStatement: sqlStatementsParser.parse('SELECT 1;')[0]
         }
       ]
-    };
+    });
+
     const newStatements = sqlStatementsParser.parse('SELECT 1;');
-    const result = syncExecutables(executor, {
+    const result = syncSqlExecutables(executor, {
       precedingStatements: [],
       activeStatement: newStatements[0],
       followingStatements: [],
@@ -143,7 +150,7 @@ describe('utils.js', () => {
   });
 
   it('should add executables when a statement is added', () => {
-    const executor = {
+    const executor = mockExecutor({
       database: () => 'someDb',
       executables: [
         {
@@ -151,9 +158,10 @@ describe('utils.js', () => {
           parsedStatement: sqlStatementsParser.parse('SELECT 1;')[0]
         }
       ]
-    };
+    });
+
     const newStatements = sqlStatementsParser.parse('SELECT 1;SELECT 2');
-    const result = syncExecutables(executor, {
+    const result = syncSqlExecutables(executor, {
       precedingStatements: [newStatements[0]],
       activeStatement: newStatements[1],
       followingStatements: [],
@@ -168,7 +176,7 @@ describe('utils.js', () => {
 
   it('should mark removed statements as lost', () => {
     const initialStatements = sqlStatementsParser.parse('SELECT 1;SELECT 2;');
-    const executor = {
+    const executor = mockExecutor({
       database: () => 'someDb',
       executables: [
         {
@@ -180,9 +188,9 @@ describe('utils.js', () => {
           parsedStatement: initialStatements[1]
         }
       ]
-    };
+    });
     const newStatements = sqlStatementsParser.parse('SELECT 1;');
-    const result = syncExecutables(executor, {
+    const result = syncSqlExecutables(executor, {
       precedingStatements: [],
       activeStatement: newStatements[0],
       followingStatements: [],

+ 21 - 13
desktop/core/src/desktop/js/apps/notebook2/execution/utils.js → desktop/core/src/desktop/js/apps/notebook2/execution/utils.ts

@@ -14,23 +14,30 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import Executable from 'apps/notebook2/execution/executable';
+import Executor from 'apps/notebook2/execution/executor';
 import SqlExecutable from 'apps/notebook2/execution/sqlExecutable';
+import { StatementDetails } from 'parse/types';
 
-/**
- * @param executor
- * @param statementDetails
- * @param [Snippet] snippet - Optional Snippet for history
- * @return {{all: [], edited: [], lost: [], selected: []}}
- */
-export const syncExecutables = (executor, statementDetails, snippet) => {
+export interface SyncSqlExecutablesResult {
+  all: SqlExecutable[];
+  edited: SqlExecutable[];
+  lost: SqlExecutable[];
+  selected: SqlExecutable[];
+}
+
+export const syncSqlExecutables = (
+  executor: Executor,
+  statementDetails: StatementDetails
+): SyncSqlExecutablesResult => {
   const allNewStatements = statementDetails.precedingStatements.concat(
     statementDetails.activeStatement,
     statementDetails.followingStatements
   );
 
-  const existingExecutables = executor.executables.concat();
+  const existingExecutables: (Executable | undefined)[] = executor.executables.concat();
 
-  const result = {
+  const result: SyncSqlExecutablesResult = {
     all: [],
     edited: [],
     lost: [],
@@ -47,7 +54,7 @@ export const syncExecutables = (executor, statementDetails, snippet) => {
       }
     }
 
-    let executable = existingExecutables[index];
+    let executable = existingExecutables[index] as SqlExecutable;
     if (executable) {
       const edited =
         executable.database !== activeDatabase ||
@@ -62,8 +69,7 @@ export const syncExecutables = (executor, statementDetails, snippet) => {
       executable = new SqlExecutable({
         parsedStatement: parsedStatement,
         database: activeDatabase,
-        executor: executor,
-        snippet: snippet
+        executor: executor
       });
     }
     result.all.push(executable);
@@ -77,7 +83,9 @@ export const syncExecutables = (executor, statementDetails, snippet) => {
     }
   });
 
-  result.lost = existingExecutables.filter(executable => typeof executable !== 'undefined');
+  result.lost = existingExecutables.filter(
+    executable => typeof executable !== 'undefined'
+  ) as SqlExecutable[];
 
   return result;
 };

+ 2 - 2
desktop/core/src/desktop/js/catalog/catalogUtils.js

@@ -62,11 +62,11 @@ const setSilencedErrors = options => {
 /**
  * Helper function to apply the cancellable option to an existing or new promise
  *
- * @param {CancellablePromise} [promise]
+ * @param {CancellableJqPromise} [promise]
  * @param {Object} [options]
  * @param {boolean} [options.cancellable] - Default false
  *
- * @return {CancellablePromise}
+ * @return {CancellableJqPromise}
  */
 const applyCancellable = (promise, options) => {
   if (promise && promise.preventCancel && (!options || !options.cancellable)) {

+ 5 - 5
desktop/core/src/desktop/js/catalog/dataCatalog.js

@@ -17,7 +17,7 @@
 import $ from 'jquery';
 import localforage from 'localforage';
 
-import CancellablePromise from 'api/cancellablePromise';
+import CancellableJqPromise from 'api/cancellableJqPromise';
 import catalogUtils from 'catalog/catalogUtils';
 import DataCatalogEntry from 'catalog/dataCatalogEntry';
 import GeneralDataCatalog from 'catalog/generalDataCatalog';
@@ -276,7 +276,7 @@ export class DataCatalog {
    * @param {boolean} [options.silenceErrors] - Default true
    * @param {boolean} [options.cancellable] - Default false
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   loadOptimizerPopularityForTables(options) {
     const deferred = $.Deferred();
@@ -404,7 +404,7 @@ export class DataCatalog {
     });
 
     return catalogUtils.applyCancellable(
-      new CancellablePromise(deferred, cancellablePromises),
+      new CancellableJqPromise(deferred, cancellablePromises),
       options
     );
   }
@@ -887,7 +887,7 @@ export default {
    * @param {boolean} [options.refreshCache]
    * @param {boolean} [options.cancellable] - Default false
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   getChildren: function (options) {
     const deferred = $.Deferred();
@@ -900,7 +900,7 @@ export default {
         );
       })
       .fail(deferred.reject);
-    return new CancellablePromise(deferred, undefined, cancellablePromises);
+    return new CancellableJqPromise(deferred, undefined, cancellablePromises);
   },
 
   /**

+ 37 - 37
desktop/core/src/desktop/js/catalog/dataCatalogEntry.js

@@ -18,7 +18,7 @@ import $ from 'jquery';
 import * as ko from 'knockout';
 
 import apiHelper from 'api/apiHelper';
-import CancellablePromise from 'api/cancellablePromise';
+import CancellableJqPromise from 'api/cancellableJqPromise';
 import catalogUtils from 'catalog/catalogUtils';
 import huePubSub from 'utils/huePubSub';
 import I18n from 'utils/i18n';
@@ -32,7 +32,7 @@ import { DataCatalog } from './dataCatalog';
  * @param {Object} [options]
  * @param {boolean} [options.silenceErrors]
  *
- * @return {CancellablePromise}
+ * @return {CancellableJqPromise}
  */
 const reloadSourceMeta = function (dataCatalogEntry, options) {
   if (dataCatalogEntry.dataCatalog.invalidatePromise) {
@@ -48,7 +48,7 @@ const reloadSourceMeta = function (dataCatalogEntry, options) {
     });
     return dataCatalogEntry.trackedPromise(
       'sourceMetaPromise',
-      new CancellablePromise(deferred, undefined, cancellablePromises)
+      new CancellableJqPromise(deferred, undefined, cancellablePromises)
     );
   }
 
@@ -65,7 +65,7 @@ const reloadSourceMeta = function (dataCatalogEntry, options) {
  * @param {Object} [apiOptions]
  * @param {boolean} [apiOptions.silenceErrors] - Default true
  *
- * @return {CancellablePromise}
+ * @return {CancellableJqPromise}
  */
 const reloadNavigatorMeta = function (dataCatalogEntry, apiOptions) {
   if (dataCatalogEntry.canHaveNavigatorMetadata()) {
@@ -97,7 +97,7 @@ const reloadNavigatorMeta = function (dataCatalogEntry, apiOptions) {
  * @param {boolean} [apiOptions.silenceErrors]
  * @param {boolean} [apiOptions.refreshAnalysis]
  *
- * @return {CancellablePromise}
+ * @return {CancellableJqPromise}
  */
 const reloadAnalysis = function (dataCatalogEntry, apiOptions) {
   return dataCatalogEntry.trackedPromise(
@@ -118,7 +118,7 @@ const reloadAnalysis = function (dataCatalogEntry, apiOptions) {
  * @param {Object} [apiOptions]
  * @param {boolean} [apiOptions.silenceErrors]
  *
- * @return {CancellablePromise}
+ * @return {CancellableJqPromise}
  */
 const reloadPartitions = function (dataCatalogEntry, apiOptions) {
   return dataCatalogEntry.trackedPromise(
@@ -134,7 +134,7 @@ const reloadPartitions = function (dataCatalogEntry, apiOptions) {
  * @param {Object} [apiOptions]
  * @param {boolean} [apiOptions.silenceErrors]
  *
- * @return {CancellablePromise}
+ * @return {CancellableJqPromise}
  */
 const reloadSample = function (dataCatalogEntry, apiOptions) {
   return dataCatalogEntry.trackedPromise(
@@ -150,7 +150,7 @@ const reloadSample = function (dataCatalogEntry, apiOptions) {
  * @param {Object} [apiOptions]
  * @param {boolean} [apiOptions.silenceErrors] - Default true
  *
- * @return {CancellablePromise}
+ * @return {CancellableJqPromise}
  */
 const reloadOptimizerMeta = function (dataCatalogEntry, apiOptions) {
   if (dataCatalogEntry.dataCatalog.canHaveOptimizerMeta()) {
@@ -179,7 +179,7 @@ const reloadOptimizerMeta = function (dataCatalogEntry, apiOptions) {
  * @param {boolean} [options.refreshCache] - Default false
  * @param {boolean} [options.cancellable] - Default false
  * @param {string} functionName - The function to call, i.e. 'getTopAggs' etc.
- * @return {CancellablePromise}
+ * @return {CancellableJqPromise}
  */
 const getFromMultiTableCatalog = function (catalogEntry, options, functionName) {
   const deferred = $.Deferred();
@@ -199,7 +199,7 @@ const getFromMultiTableCatalog = function (catalogEntry, options, functionName)
       );
     })
     .fail(deferred.reject);
-  return new CancellablePromise(deferred, undefined, cancellablePromises);
+  return new CancellableJqPromise(deferred, undefined, cancellablePromises);
 };
 
 /**
@@ -290,13 +290,13 @@ class DataCatalogEntry {
    * Helper function that ensure that cancellable promises are not tracked anymore when cancelled
    *
    * @param {string} promiseName - The attribute name to use
-   * @param {CancellablePromise} cancellablePromise
+   * @param {CancellableJqPromise} cancellableJqPromise
    */
-  trackedPromise(promiseName, cancellablePromise) {
+  trackedPromise(promiseName, cancellableJqPromise) {
     const self = this;
-    self[promiseName] = cancellablePromise;
-    return cancellablePromise.fail(() => {
-      if (cancellablePromise.cancelled) {
+    self[promiseName] = cancellableJqPromise;
+    return cancellableJqPromise.fail(() => {
+      if (cancellableJqPromise.cancelled) {
         delete self[promiseName];
       }
     });
@@ -309,7 +309,7 @@ class DataCatalogEntry {
    * @param {boolean} [options.cascade] - Default false, only used when the entry is for the source
    * @param {boolean} [options.silenceErrors] - Default false
    * @param {string} [options.targetChild] - Optional specific child to invalidate
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   clearCache(options) {
     const self = this;
@@ -334,7 +334,7 @@ class DataCatalogEntry {
       });
     });
 
-    return new CancellablePromise(saveDeferred, undefined, []);
+    return new CancellableJqPromise(saveDeferred, undefined, []);
   }
 
   /**
@@ -387,7 +387,7 @@ class DataCatalogEntry {
    * @param {boolean} [options.refreshCache]
    * @param {boolean} [options.cancellable] - Default false
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   getChildren(options) {
     const self = this;
@@ -520,7 +520,7 @@ class DataCatalogEntry {
     return catalogUtils.applyCancellable(
       self.trackedPromise(
         'childrenPromise',
-        new CancellablePromise(deferred, undefined, [sourceMetaPromise])
+        new CancellableJqPromise(deferred, undefined, [sourceMetaPromise])
       ),
       options
     );
@@ -534,7 +534,7 @@ class DataCatalogEntry {
    * @param {boolean} [options.silenceErrors] - Default true
    * @param {boolean} [options.cancellable] - Default false
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   loadNavigatorMetaForChildren(options) {
     const self = this;
@@ -638,7 +638,7 @@ class DataCatalogEntry {
     return catalogUtils.applyCancellable(
       self.trackedPromise(
         'navigatorMetaForChildrenPromise',
-        new CancellablePromise(deferred, null, cancellablePromises)
+        new CancellableJqPromise(deferred, null, cancellablePromises)
       ),
       options
     );
@@ -651,7 +651,7 @@ class DataCatalogEntry {
    * @param {Object} [options]
    * @param {boolean} [options.silenceErrors] - Default false
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   applyOptimizerResponseToChildren(response, options) {
     const self = this;
@@ -711,7 +711,7 @@ class DataCatalogEntry {
       })
       .fail(deferred.reject);
 
-    return new CancellablePromise(deferred, undefined, [childPromise]);
+    return new CancellableJqPromise(deferred, undefined, [childPromise]);
   }
 
   /**
@@ -722,7 +722,7 @@ class DataCatalogEntry {
    * @param {boolean} [options.silenceErrors] - Default true
    * @param {boolean} [options.cancellable] - Default false
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   loadOptimizerPopularityForChildren(options) {
     const self = this;
@@ -784,7 +784,7 @@ class DataCatalogEntry {
     return catalogUtils.applyCancellable(
       self.trackedPromise(
         'optimizerPopularityForChildrenPromise',
-        new CancellablePromise(deferred, undefined, cancellablePromises)
+        new CancellableJqPromise(deferred, undefined, cancellablePromises)
       ),
       options
     );
@@ -858,7 +858,7 @@ class DataCatalogEntry {
    * @param {boolean} [options.refreshCache]
    * @param {boolean} [options.cancellable] - Default false
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   getComment(options) {
     const self = this;
@@ -916,7 +916,7 @@ class DataCatalogEntry {
     }
 
     return catalogUtils.applyCancellable(
-      new CancellablePromise(deferred, undefined, cancellablePromises),
+      new CancellableJqPromise(deferred, undefined, cancellablePromises),
       options
     );
   }
@@ -1463,7 +1463,7 @@ class DataCatalogEntry {
    * @param {boolean} [options.refreshCache]
    * @param {boolean} [options.cancellable] - Default false
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   getSourceMeta(options) {
     const self = this;
@@ -1492,7 +1492,7 @@ class DataCatalogEntry {
    * @param {boolean} [options.refreshAnalysis] - Performs a hard refresh on the source level
    * @param {boolean} [options.cancellable] - Default false
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   getAnalysis(options) {
     const self = this;
@@ -1520,7 +1520,7 @@ class DataCatalogEntry {
    * @param {boolean} [options.refreshCache] - Clears the browser cache
    * @param {boolean} [options.cancellable] - Default false
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   getPartitions(options) {
     const self = this;
@@ -1551,7 +1551,7 @@ class DataCatalogEntry {
    * @param {boolean} [options.refreshCache]
    * @param {boolean} [options.cancellable] - Default false
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   getNavigatorMeta(options) {
     const self = this;
@@ -1585,7 +1585,7 @@ class DataCatalogEntry {
    * @param {boolean} [options.refreshCache] - Default false
    * @param {boolean} [options.cancellable] - Default false
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   getOptimizerMeta(options) {
     const self = this;
@@ -1621,7 +1621,7 @@ class DataCatalogEntry {
    * @param {boolean} [options.cancellable] - Default false
    * @oaram {string} [options.operation]
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   getSample(options) {
     const self = this;
@@ -1708,7 +1708,7 @@ class DataCatalogEntry {
       return catalogUtils.applyCancellable(
         self.trackedPromise(
           'samplePromise',
-          new CancellablePromise(deferred, undefined, cancellablePromises)
+          new CancellableJqPromise(deferred, undefined, cancellablePromises)
         ),
         options
       );
@@ -1738,7 +1738,7 @@ class DataCatalogEntry {
    * @param {boolean} [options.refreshCache] - Default false
    * @param {boolean} [options.cancellable] - Default false
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   getTopAggs(options) {
     const self = this;
@@ -1754,7 +1754,7 @@ class DataCatalogEntry {
    * @param {boolean} [options.refreshCache] - Default false
    * @param {boolean} [options.cancellable] - Default false
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   getTopFilters(options) {
     const self = this;
@@ -1770,7 +1770,7 @@ class DataCatalogEntry {
    * @param {boolean} [options.refreshCache] - Default false
    * @param {boolean} [options.cancellable] - Default false
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   getTopJoins(options) {
     const self = this;

+ 11 - 11
desktop/core/src/desktop/js/catalog/multiTableEntry.js

@@ -29,7 +29,7 @@ import { DataCatalog } from './dataCatalog';
  * @param {string} promiseAttribute
  * @param {string} dataAttribute
  * @param {Function} apiHelperFunction
- * @return {CancellablePromise}
+ * @return {CancellableJqPromise}
  */
 const genericOptimizerReload = function (
   multiTableEntry,
@@ -60,7 +60,7 @@ const genericOptimizerReload = function (
  * @param {string} promiseAttribute
  * @param {string} dataAttribute
  * @param {Function} apiHelperFunction
- * @return {CancellablePromise}
+ * @return {CancellableJqPromise}
  */
 const genericOptimizerGet = function (
   multiTableEntry,
@@ -155,13 +155,13 @@ class MultiTableEntry {
    * Helper function that ensure that cancellable promises are not tracked anymore when cancelled
    *
    * @param {string} promiseName - The attribute name to use
-   * @param {CancellablePromise} cancellablePromise
+   * @param {CancellableJqPromise} cancellableJqPromise
    */
-  trackedPromise(promiseName, cancellablePromise) {
+  trackedPromise(promiseName, cancellableJqPromise) {
     const self = this;
-    self[promiseName] = cancellablePromise;
-    return cancellablePromise.fail(() => {
-      if (cancellablePromise.cancelled) {
+    self[promiseName] = cancellableJqPromise;
+    return cancellableJqPromise.fail(() => {
+      if (cancellableJqPromise.cancelled) {
         delete self[promiseName];
       }
     });
@@ -194,7 +194,7 @@ class MultiTableEntry {
    * @param {boolean} [options.refreshCache] - Default false
    * @param {boolean} [options.cancellable] - Default false
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   getTopAggs(options) {
     const optimizer = getOptimizer(this.dataCatalog.connector);
@@ -216,7 +216,7 @@ class MultiTableEntry {
    * @param {boolean} [options.refreshCache] - Default false
    * @param {boolean} [options.cancellable] - Default false
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   getTopColumns(options) {
     const optimizer = getOptimizer(this.dataCatalog.connector);
@@ -238,7 +238,7 @@ class MultiTableEntry {
    * @param {boolean} [options.refreshCache] - Default false
    * @param {boolean} [options.cancellable] - Default false
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   getTopFilters(options) {
     const optimizer = getOptimizer(this.dataCatalog.connector);
@@ -260,7 +260,7 @@ class MultiTableEntry {
    * @param {boolean} [options.refreshCache] - Default false
    * @param {boolean} [options.cancellable] - Default false
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   getTopJoins(options) {
     const optimizer = getOptimizer(this.dataCatalog.connector);

+ 5 - 5
desktop/core/src/desktop/js/catalog/optimizer/apiStrategy.js

@@ -17,7 +17,7 @@
 import $ from 'jquery';
 import * as ko from 'knockout';
 
-import CancellablePromise from 'api/cancellablePromise';
+import CancellableJqPromise from 'api/cancellableJqPromise';
 import { simplePost } from 'api/apiUtils';
 import { OPTIMIZER_API } from 'api/urls';
 import BaseStrategy from './baseStrategy';
@@ -27,7 +27,7 @@ import BaseStrategy from './baseStrategy';
  *
  * @param {OptimizerOptions} options
  * @param {string} url
- * @return {CancellablePromise}
+ * @return {CancellableJqPromise}
  */
 const genericOptimizerMultiTableFetch = (options, url) => {
   const deferred = $.Deferred();
@@ -49,7 +49,7 @@ const genericOptimizerMultiTableFetch = (options, url) => {
     errorCallback: deferred.reject
   });
 
-  return new CancellablePromise(deferred, request);
+  return new CancellableJqPromise(deferred, request);
 };
 
 export default class ApiStrategy extends BaseStrategy {
@@ -106,7 +106,7 @@ export default class ApiStrategy extends BaseStrategy {
       errorCallback: deferred.reject
     });
 
-    return new CancellablePromise(deferred, request);
+    return new CancellableJqPromise(deferred, request);
   }
 
   fetchTopAggs(options) {
@@ -148,6 +148,6 @@ export default class ApiStrategy extends BaseStrategy {
       }
     );
 
-    return new CancellablePromise(deferred, request);
+    return new CancellableJqPromise(deferred, request);
   }
 }

+ 13 - 13
desktop/core/src/desktop/js/catalog/optimizer/baseStrategy.js

@@ -16,7 +16,7 @@
 
 import $ from 'jquery';
 
-import CancellablePromise from 'api/cancellablePromise';
+import CancellableJqPromise from 'api/cancellableJqPromise';
 
 export default class BaseStrategy {
   constructor(connector) {
@@ -48,50 +48,50 @@ export default class BaseStrategy {
    * Fetches optimizer popularity for the children of the given path
    *
    * @param {OptimizerOptions} options
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   fetchPopularity(options) {
-    return new CancellablePromise($.Deferred().reject());
+    return new CancellableJqPromise($.Deferred().reject());
   }
 
   /**
    * Fetches the popular aggregate functions for the given tables
    *
    * @param {OptimizerOptions} options
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   fetchTopAggs(options) {
-    return new CancellablePromise($.Deferred().reject());
+    return new CancellableJqPromise($.Deferred().reject());
   }
 
   /**
    * Fetches the popular columns for the given tables
    *
    * @param {OptimizerOptions} options
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   fetchTopColumns(options) {
-    return new CancellablePromise($.Deferred().reject());
+    return new CancellableJqPromise($.Deferred().reject());
   }
 
   /**
    * Fetches the popular filters for the given tables
    *
    * @param {OptimizerOptions} options
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   fetchTopFilters(options) {
-    return new CancellablePromise($.Deferred().reject());
+    return new CancellableJqPromise($.Deferred().reject());
   }
 
   /**
    * Fetches the popular joins for the given tables
    *
    * @param {OptimizerOptions} options
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   fetchTopJoins(options) {
-    return new CancellablePromise($.Deferred().reject());
+    return new CancellableJqPromise($.Deferred().reject());
   }
 
   /**
@@ -101,9 +101,9 @@ export default class BaseStrategy {
    * @param {boolean} [options.silenceErrors]
    * @param {string[]} options.path
    *
-   * @return {CancellablePromise}
+   * @return {CancellableJqPromise}
    */
   fetchOptimizerMeta(options) {
-    return new CancellablePromise($.Deferred().reject());
+    return new CancellableJqPromise($.Deferred().reject());
   }
 }

+ 2 - 2
desktop/core/src/desktop/js/hue.js

@@ -40,7 +40,7 @@ import 'parse/parserTypeDefs';
 import 'utils/customIntervals';
 import 'utils/json.bigDataParse';
 import apiHelper from 'api/apiHelper';
-import CancellablePromise from 'api/cancellablePromise';
+import CancellableJqPromise from 'api/cancellableJqPromise';
 import { DOCUMENT_TYPE_I18n, DOCUMENT_TYPES } from 'doc/docSupport';
 import contextCatalog from 'catalog/contextCatalog';
 import dataCatalog from 'catalog/dataCatalog';
@@ -96,7 +96,7 @@ import { simpleGet } from 'api/apiUtils'; // In analytics.mako, metrics.mako, th
 window._ = _;
 window.apiHelper = apiHelper;
 window.simpleGet = simpleGet;
-window.CancellablePromise = CancellablePromise;
+window.CancellableJqPromise = CancellableJqPromise;
 window.contextCatalog = contextCatalog;
 window.d3 = d3;
 window.d3v3 = d3v3;

+ 12 - 0
desktop/core/src/desktop/js/parse/sqlStatementsParser.d.ts

@@ -0,0 +1,12 @@
+import { ParsedLocation } from 'parse/types';
+
+declare module 'parse/sqlStatementsParser' {
+  export interface ParsedSqlStatement {
+    firstToken: string;
+    statement: string;
+    location: ParsedLocation;
+    type: string;
+  }
+
+  export function parse(statement: string): ParsedSqlStatement[];
+}

+ 15 - 0
desktop/core/src/desktop/js/parse/types.ts

@@ -14,6 +14,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import { ParsedSqlStatement } from './sqlStatementsParser';
+
 export interface IdentifierChainEntry {
   name: string;
 }
@@ -29,3 +31,16 @@ export interface ParsedLocation {
   last_line: number;
   last_column: number;
 }
+
+export interface StatementDetails {
+  selectedStatements: ParsedSqlStatement[];
+  precedingStatements: ParsedSqlStatement[];
+  activeStatement: ParsedSqlStatement;
+  followingStatements: ParsedSqlStatement[];
+}
+
+export interface SqlStatementsParser {
+  parse(text: string): ParsedSqlStatement;
+}
+
+declare const sqlStatementsParser: SqlStatementsParser;

+ 4 - 4
desktop/core/src/desktop/js/sql/sqlUtils.ts

@@ -17,7 +17,7 @@
 import DataCatalogEntry from 'catalog/dataCatalogEntry';
 import $ from 'jquery';
 
-import CancellablePromise from 'api/cancellablePromise';
+import CancellableJqPromise from 'api/cancellableJqPromise';
 import dataCatalog from 'catalog/dataCatalog';
 import { IdentifierChainEntry, ParsedLocation, ParsedTable } from 'parse/types';
 import { isReserved } from 'sql/reference/sqlReferenceRepository';
@@ -125,10 +125,10 @@ export const resolveCatalogEntry = (options: {
   cancellable?: boolean;
   identifierChain?: IdentifierChainEntry[];
   tables?: ParsedTable[];
-}): CancellablePromise<DataCatalogEntry> => {
-  const cancellablePromises: CancellablePromise<unknown>[] = [];
+}): CancellableJqPromise<DataCatalogEntry> => {
+  const cancellablePromises: CancellableJqPromise<unknown>[] = [];
   const deferred = $.Deferred();
-  const promise = new CancellablePromise(deferred, undefined, cancellablePromises);
+  const promise = new CancellableJqPromise(deferred, undefined, cancellablePromises);
   dataCatalog.applyCancellable(promise, { cancellable: !!options.cancellable });
 
   if (!options.identifierChain) {

+ 4 - 0
desktop/core/src/desktop/js/utils/json.bigDataParse.d.ts

@@ -0,0 +1,4 @@
+interface JSON {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  bigdataParse(text: string, reviver?: (this: any, key: string, value: any) => any): any;
+}

+ 3 - 3
package-lock.json

@@ -9889,9 +9889,9 @@
       }
     },
     "eslint-plugin-vue": {
-      "version": "7.0.0-beta.3",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.0.0-beta.3.tgz",
-      "integrity": "sha512-/p23IRPN9gFNN7dzesrctt06Kvs9E3VRB8BGIAPSEaQNk5yhlKUzntPARjUpsTWW+DQg0mqglZptfkUJK4+4EQ==",
+      "version": "7.0.0-beta.2",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.0.0-beta.2.tgz",
+      "integrity": "sha512-EpAVWT62JTM7Yo6IUJIaIvNMSuayDaMVMXv1SC96d1/7nyx18U0FrLRKpN2GdTY8Efi4UepgR75XW7ucSRiI7A==",
       "dev": true,
       "requires": {
         "eslint-utils": "^2.1.0",

+ 1 - 1
tsconfig.json

@@ -11,7 +11,7 @@
     "esModuleInterop": true,
     "baseUrl": "desktop/core/src/desktop/js",
     "typeRoots": [ "desktop/core/src/desktop/js/types", "./node_modules/@types"],
-    "lib": ["es2019"]
+    "lib": ["es2019", "dom"]
   },
   "include": [
     "."