浏览代码

HUE-8127 [assist] Fix issue where the context popover throws js error for complex entries

This also adds a helper method on locations to resolve the corresponding catalogEntry and takes care of an issue where the ApiHelper simplePost promise gets resolved on HTTP 200 responses that are actually errors.
Johan Ahlen 7 年之前
父节点
当前提交
9dbc495308

+ 30 - 6
desktop/core/src/desktop/static/desktop/js/apiHelper.js

@@ -385,8 +385,11 @@ var ApiHelper = (function () {
   };
 
   ApiHelper.prototype.cancelActiveRequest = function (request) {
-    if (typeof request !== 'undefined' && request !== null && request.readyState < 4) {
-      request.abort();
+    if (typeof request !== 'undefined' && request !== null) {
+      var readyState = request.getReadyState ? request.getReadyState() : request.readyState;
+      if (readyState < 4) {
+        request.abort();
+      }
     }
   };
 
@@ -400,14 +403,35 @@ var ApiHelper = (function () {
    */
   ApiHelper.prototype.simplePost = function (url, data, options) {
     var self = this;
-    return $.post(url, data, function (data) {
+    var deferred = $.Deferred();
+
+    var request = $.post(url, data, function (data) {
       if (self.successResponseIsError(data)) {
-        self.assistErrorCallback(options)(data);
-      } else if (options && options.successCallback) {
+        deferred.reject(self.assistErrorCallback(options)(data));
+        return;
+      }
+      if (options && options.successCallback) {
         options.successCallback(data);
       }
+      deferred.resolve(data);
     })
     .fail(self.assistErrorCallback(options));
+
+    request.fail(function (data) {
+      deferred.reject(self.assistErrorCallback(options)(data));
+    });
+
+    var promise = deferred.promise();
+
+    promise.getReadyState = function () {
+      return request.readyState;
+    };
+
+    promise.abort = function () {
+      request.abort();
+    };
+
+    return promise;
   };
 
   /**
@@ -1516,7 +1540,7 @@ var ApiHelper = (function () {
     self.id = UUID();
     self.type = sourceType;
     self.status = response.status || 'running';
-    self.result = response.result;
+    self.result = response.result || {};
     self.result.type = 'table';
   };
 

+ 3 - 1
desktop/core/src/desktop/static/desktop/js/dataCatalog.js

@@ -2054,7 +2054,9 @@ var DataCatalog = (function () {
 
       disableCache: function () {
         cacheEnabled = false;
-      }
+      },
+
+      applyCancellable: applyCancellable
     };
   })();
 })();

+ 5 - 7
desktop/core/src/desktop/static/desktop/js/ko.hue-bindings.js

@@ -4053,7 +4053,7 @@
             } else if (location.tables) {
               location.tables.some(function (table) {
                 if (table.identifierChain && table.identifierChain.length === 1 && table.identifierChain[0].name) {
-                  possibleAlias = aliasIndex[table.identifierChain[0].name.toLowerCase()]
+                  possibleAlias = aliasIndex[table.identifierChain[0].name.toLowerCase()];
                   return possibleAlias;
                 }
                 return false;
@@ -4182,7 +4182,7 @@
         };
 
         // Clear out old parse locations to prevent them from being shown when there's a syntax error in the statement
-        while(activeTokens.length > 0) {
+        while (activeTokens.length > 0) {
           delete activeTokens.pop().parseLocation;
         }
 
@@ -4811,11 +4811,7 @@
               };
 
               if (token.parseLocation && token.parseLocation.identifierChain && !token.notFound) {
-                // Database, table and field
-                var path = $.map(token.parseLocation.identifierChain, function (identifier) {
-                  return identifier.name;
-                });
-                DataCatalog.getEntry({sourceType: snippet.type(), path: path}).done(function (entry) {
+                token.parseLocation.resolveCatalogEntry().done(function (entry) {
                   huePubSub.publish('context.popover.show', {
                     data: {
                       type: 'catalogEntry',
@@ -4824,6 +4820,8 @@
                     pinEnabled: true,
                     source: source
                   });
+                }).fail(function () {
+                  token.notFound = true;
                 });
               } else if (token.parseLocation && !token.notFound) {
                 // Asterisk, function etc.

+ 122 - 1
desktop/core/src/desktop/static/desktop/js/sqlUtils.js

@@ -110,6 +110,125 @@ var SqlUtils = (function () {
     });
   };
 
+  var identifierChainToPath = function (identifierChain) {
+    return $.map(identifierChain, function (identifier) {
+      return identifier.name
+    })
+  };
+
+  /**
+   *
+   * @param {Object} options
+   * @param {Object[]} [options.identifierChain]
+   * @param {Object[]} [options.tables]
+   * @param {String} options.sourceType
+   * @param {Object} [options.cancellable]
+   * @param {Object} [options.cachedOnly]
+   *
+   * @return {CancellablePromise}
+   */
+  var resolveCatalogEntry = function (options) {
+    var cancellablePromises = [];
+    var deferred = $.Deferred();
+    var promise = new CancellablePromise(deferred, undefined, cancellablePromises);
+    DataCatalog.applyCancellable(promise, options);
+
+    if (!options.identifierChain) {
+      deferred.reject();
+      return promise;
+    }
+
+    var findInTree = function (currentEntry, fieldsToGo) {
+      if (fieldsToGo.length === 0) {
+        deferred.reject();
+        return;
+      }
+
+      var nextField;
+      if (currentEntry.getType() === 'map') {
+        nextField = 'value';
+      } else if (currentEntry.getType() === 'array') {
+        nextField = 'item';
+      } else {
+        nextField = fieldsToGo.shift();
+      }
+
+      cancellablePromises.push(currentEntry.getChildren({
+        cancellable: options.cancellable,
+        cachedOnly: options.cachedOnly,
+        silenceErrors: true
+      }).done(function (childEntries) {
+        var foundEntry = undefined;
+        childEntries.some(function (childEntry) {
+          if (SqlUtils.identifierEquals(childEntry.name, nextField)) {
+            foundEntry = childEntry;
+            return true;
+          }
+        });
+        if (foundEntry && fieldsToGo.length) {
+          findInTree(foundEntry, fieldsToGo);
+        } else if (foundEntry) {
+          deferred.resolve(foundEntry);
+        } else {
+          deferred.reject();
+        }
+      }).fail(deferred.reject))
+    };
+
+    var findTable = function (tablesToGo) {
+      if (tablesToGo.length === 0) {
+        deferred.reject();
+        return;
+      }
+
+      var nextTable = tablesToGo.pop();
+      if (typeof nextTable.subQuery !== 'undefined') {
+        findTable(tablesToGo);
+        return;
+      }
+
+      cancellablePromises.push(DataCatalog.getChildren({
+        sourceType: options.sourceType,
+        path: SqlUtils.identifierChainToPath(nextTable.identifierChain),
+        cachedOnly: options && options.cachedOnly,
+        cancellable: options && options.cancellable,
+        silenceErrors: true
+      }).done(function (childEntries) {
+        var foundEntry = undefined;
+        childEntries.some(function (childEntry) {
+          if (SqlUtils.identifierEquals(childEntry.name, options.identifierChain[0].name)) {
+            foundEntry = childEntry;
+            return true;
+          }
+        });
+
+        if (foundEntry && options.identifierChain.length > 1) {
+          findInTree(foundEntry, SqlUtils.identifierChainToPath(options.identifierChain.slice(1)));
+        } else if (foundEntry) {
+          deferred.resolve(foundEntry);
+        } else {
+          findTable(tablesToGo)
+        }
+      }).fail(deferred.reject));
+    };
+
+    if (options.tables) {
+      findTable(options.tables.concat())
+    } else {
+      DataCatalog.getEntry({
+        sourceType: options.sourceType,
+        path: [],
+        cachedOnly: options && options.cachedOnly,
+        cancellable: options && options.cancellable,
+        silenceErrors: true
+      }).done(function (entry) {
+        findInTree(entry, SqlUtils.identifierChainToPath(options.identifierChain))
+      })
+    }
+
+    return promise;
+  };
+
   return {
     autocompleteFilter : autocompleteFilter,
     backTickIfNeeded: function (sourceType, identifier) {
@@ -137,6 +256,8 @@ var SqlUtils = (function () {
     identifierEquals: function (a, b) {
       return a && b && a.replace(/^\s*`/, '').replace(/`\s*$/, '').toLowerCase() === b.replace(/^\s*`/, '').replace(/`\s*$/, '').toLowerCase();
     },
-    sortSuggestions: sortSuggestions
+    sortSuggestions: sortSuggestions,
+    resolveCatalogEntry: resolveCatalogEntry,
+    identifierChainToPath: identifierChainToPath
   }
 })();

+ 1 - 0
desktop/core/src/desktop/templates/ace_sql_location_worker.mako

@@ -100,6 +100,7 @@ importScripts(scriptPrefix + '${ static('desktop/js/sqlFunctions.js') }');
 
         postMessage({
           id: msg.data.id,
+          sourceType: msg.data.type,
           editorChangeTime: msg.data.statementDetails.editorChangeTime,
           locations: locations,
           activeStatementLocations: activeStatementLocations,

+ 3 - 1
desktop/libs/notebook/src/notebook/static/notebook/js/notebook.ko.js

@@ -763,6 +763,8 @@ var EditorViewModel = (function() {
         var re = /\${(\w*)\=?([^{}]*)}/g;
         if (location.type === 'variable' && location.colRef) {
           var column = location.colRef.identifierChain;
+          // TODO: This should support multiple tables, i.e. SELECT * FROM web_logs, customers WHERE id = ${id}
+          //       use "location.resolveCatalogEntry({ cancellable: true });"
           var identifierChain = location.colRef.tables[0].identifierChain.slice().concat(column);
           var value = re.exec(location.value);
           variables.push({
@@ -833,7 +835,7 @@ var EditorViewModel = (function() {
             }).then(updateVariableType.bind(self, variable)));
           });
         } else {
-          updateVariableType([variable.name()], variable, {
+          updateVariableType(variable, {
             type: 'text'
           });
         }

+ 42 - 0
desktop/libs/notebook/src/notebook/templates/editor_components.mako

@@ -3249,6 +3249,38 @@ function togglePresentation(value) {};
       ko.applyBindings(viewModel, $('#${ bindableElement }')[0]);
       viewModel.init();
 
+      var attachEntryResolver = function (location, sourceType) {
+        location.resolveCatalogEntry = function(options) {
+          if (!options) {
+            options = {};
+          }
+          if (location.resolvePathPromise && !location.resolvePathPromise.cancelled) {
+            DataCatalog.applyCancellable(location.resolvePathPromise, options);
+            return location.resolvePathPromise;
+          }
+
+          if (!location.identifierChain) {
+            if (!location.resolvePathPromise) {
+              location.resolvePathPromise = $.Deferred().reject().promise();
+            }
+            return location.resolvePathPromise;
+          }
+
+          var promise = SqlUtils.resolveCatalogEntry({
+            sourceType: sourceType,
+            cancellable: options.cancellable,
+            cachedOnly: options.cachedOnly,
+            identifierChain: location.identifierChain,
+            tables: location.tables
+          });
+
+          if (!options.cachedOnly) {
+            location.resolvePathPromise = promise;
+          }
+          return promise;
+        }
+      };
+
 
       % if not IS_EMBEDDED.get():
         if (window.Worker) {
@@ -3285,6 +3317,11 @@ function togglePresentation(value) {};
             if (e.data.ping) {
               aceSqlLocationWorker.isReady = true;
             } else {
+              if (e.data.locations) {
+                e.data.locations.forEach(function (location) {
+                  attachEntryResolver(location, e.data.sourceType);
+                })
+              }
               huePubSub.publish('ace.sql.location.worker.message', e);
             }
           };
@@ -3302,6 +3339,11 @@ function togglePresentation(value) {};
 
         window.addEventListener("message", function (event) {
           if (event.data.locationWorkerResponse) {
+            if (event.data.locationWorkerResponse.locations) {
+              event.data.locationWorkerResponse.locations.forEach(function (location) {
+                attachEntryResolver(location, event.data.locationWorkerResponse.sourceType);
+              })
+            }
             huePubSub.publish('ace.sql.location.worker.message', { data: event.data.locationWorkerResponse });
           }
           if (event.data.syntaxWorkerResponse) {