Bläddra i källkod

[catalog] Move autocomplete api handling to Axios and fix and issue where an error is shown on missing entity

Johan Ahlen 5 år sedan
förälder
incheckning
24784de6db

+ 0 - 64
desktop/core/src/desktop/js/api/apiHelper.js

@@ -1246,70 +1246,6 @@ class ApiHelper {
     return deferred.resolve().promise();
   }
 
-  /**
-   * @param {Object} options
-   * @param {string} options.sourceType
-   * @param {ContextCompute} options.compute
-   * @param {boolean} [options.silenceErrors]
-   * @param {number} [options.timeout]
-   *
-   * @param {string[]} [options.path] - The path to fetch
-   *
-   * @return {CancellableJqPromise}
-   */
-  fetchSourceMetadata(options) {
-    const deferred = $.Deferred();
-
-    const isQuery = options.sourceType.indexOf('-query') !== -1;
-    const sourceType = isQuery ? options.sourceType.replace('-query', '') : options.sourceType;
-
-    const request = $.ajax({
-      type: 'POST',
-      url:
-        URLS.AUTOCOMPLETE_API_PREFIX +
-        (isQuery ? options.path.slice(1) : options.path).join('/') +
-        (options.path.length ? '/' : ''),
-      data: {
-        notebook: {},
-        snippet: ko.mapping.toJSON({
-          type: sourceType,
-          source: isQuery ? 'query' : 'data'
-        }),
-        cluster: ko.mapping.toJSON(options.compute ? options.compute : '""')
-      },
-      timeout: options.timeout
-    })
-      .done(data => {
-        data.notFound =
-          data.status === 0 &&
-          data.code === 500 &&
-          data.error &&
-          (data.error.indexOf('Error 10001') !== -1 ||
-            data.error.indexOf('AnalysisException') !== -1);
-        data.hueTimestamp = Date.now();
-
-        // TODO: Display warning in autocomplete when an entity can't be found
-        // Hive example: data.error: [...] SemanticException [Error 10001]: Table not found default.foo
-        // Impala example: data.error: [...] AnalysisException: Could not resolve path: 'default.foo'
-        if (!data.notFound && successResponseIsError(data)) {
-          assistErrorCallback({
-            silenceErrors: options.silenceErrors,
-            errorCallback: deferred.reject
-          })(data);
-        } else {
-          deferred.resolve(data);
-        }
-      })
-      .fail(
-        assistErrorCallback({
-          silenceErrors: options.silenceErrors,
-          errorCallback: deferred.reject
-        })
-      );
-
-    return new CancellableJqPromise(deferred, request);
-  }
-
   updateSourceMetadata(options) {
     let url;
     const data = {

+ 16 - 13
desktop/core/src/desktop/js/catalog/DataCatalogEntry.ts

@@ -15,6 +15,7 @@
 // limitations under the License.
 
 import { Cancellable, CancellablePromise } from 'api/cancellablePromise';
+import { fetchSourceMeta } from 'catalog/api';
 import MultiTableEntry, { TopAggs, TopFilters, TopJoins } from 'catalog/MultiTableEntry';
 import { getOptimizer } from './optimizer/optimizer';
 import * as ko from 'knockout';
@@ -231,23 +232,25 @@ const reloadSourceMeta = (
         } catch (err) {}
       }
 
-      const fetchPromise = fetchAndSave<SourceMeta>(
-        (<(options: FetchOptions) => CancellableJqPromise<SourceMeta>>(
-          (<unknown>apiHelper.fetchSourceMetadata)
-        )).bind(apiHelper),
-        val => {
-          entry.sourceMeta = val;
-        },
-        entry,
-        options
-      );
+      entry.sourceMetaPromise = fetchSourceMeta({
+        ...options,
+        entry
+      });
 
       onCancel(() => {
-        fetchPromise.cancel();
-        entry.sourceMetaPromise = undefined;
+        if (entry.sourceMetaPromise) {
+          entry.sourceMetaPromise.cancel();
+          entry.sourceMetaPromise = undefined;
+        }
       });
 
-      fetchPromise.then(resolve).fail(reject);
+      try {
+        entry.sourceMeta = await entry.sourceMetaPromise;
+        resolve(entry.sourceMeta);
+        entry.saveLater();
+      } catch (err) {
+        reject(err);
+      }
     }
   );
   return entry.sourceMetaPromise;

+ 61 - 0
desktop/core/src/desktop/js/catalog/api.ts

@@ -0,0 +1,61 @@
+// 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 { extractErrorMessage, post, successResponseIsError } from 'api/utils';
+import DataCatalogEntry, { SourceMeta } from 'catalog/DataCatalogEntry';
+
+const AUTOCOMPLETE_API_PREFIX = '/notebook/api/autocomplete/';
+
+export const fetchSourceMeta = (options: {
+  entry: DataCatalogEntry;
+  silenceErrors?: boolean;
+}): CancellablePromise<SourceMeta> => {
+  const url =
+    AUTOCOMPLETE_API_PREFIX + options.entry.path.join('/') + (options.entry.path.length ? '/' : '');
+
+  return post<SourceMeta & { status: number; code?: number; error?: string; message?: string }>(
+    url,
+    {
+      notebook: {},
+      snippet: JSON.stringify({
+        type: options.entry.getConnector().id,
+        source: 'data'
+      }),
+      cluster: (options.entry.compute && JSON.stringify(options.entry.compute)) || '""'
+    },
+    {
+      ...options,
+      handleResponse: response => {
+        const message = response.error || response.message || '';
+        const adjustedResponse = response;
+        adjustedResponse.notFound =
+          response.status === 0 &&
+          response.code === 500 &&
+          (message.indexOf('Error 10001') !== -1 || message.indexOf('AnalysisException') !== -1);
+
+        adjustedResponse.hueTimestamp = Date.now();
+
+        const valid = adjustedResponse.notFound || !successResponseIsError(response);
+
+        if (!valid) {
+          return { valid, reason: extractErrorMessage(response) };
+        }
+        return { valid, adjustedResponse };
+      }
+    }
+  );
+};

+ 3 - 5
desktop/core/src/desktop/js/ko/components/ko.pollingCatalogEntriesList.test.js

@@ -15,17 +15,15 @@
 // limitations under the License.
 
 import { koSetup } from 'jest/koTestUtils';
+import { CancellablePromise } from '../../api/cancellablePromise';
 import { NAME } from './ko.pollingCatalogEntriesList';
-import $ from 'jquery';
-import ApiHelper from 'api/apiHelper';
+import * as CatalogApi from 'catalog/api';
 
 describe('ko.pollingCatalogEntriesList.js', () => {
   const setup = koSetup();
 
   it('should render component', async () => {
-    jest
-      .spyOn(ApiHelper, 'fetchSourceMetadata')
-      .mockImplementation(() => $.Deferred().reject().promise());
+    jest.spyOn(CatalogApi, 'fetchSourceMeta').mockImplementation(() => CancellablePromise.reject());
 
     const element = await setup.renderComponent(NAME, {
       sourceType: 'impala',

+ 20 - 16
desktop/core/src/desktop/js/sql/autocompleteResults.test.js

@@ -17,7 +17,9 @@
 import $ from 'jquery';
 
 import ApiHelper from 'api/apiHelper';
+import * as CatalogApi from 'catalog/api';
 import * as apiUtils from 'sql/reference/apiUtils';
+import { CancellablePromise } from '../api/cancellablePromise';
 import AutocompleteResults from './autocompleteResults';
 import dataCatalog from 'catalog/dataCatalog';
 import huePubSub from 'utils/huePubSub';
@@ -31,40 +33,42 @@ describe('AutocompleteResults.js', () => {
     status: 500
   };
 
-  jest.spyOn(ApiHelper, 'fetchSourceMetadata').mockImplementation(options => {
-    const deferred = $.Deferred();
+  jest.spyOn(CatalogApi, 'fetchSourceMeta').mockImplementation(options => {
     if (Math.random() < 0.5) {
-      deferred.resolve(failResponse);
-    } else if (options.path.length === 0) {
-      deferred.resolve(JSON.parse('{"status": 0, "databases": ["default"]}'));
-    } else if (options.path.length === 1) {
-      deferred.resolve(
+      return CancellablePromise.reject(failResponse);
+    }
+    if (options.entry.path.length === 0) {
+      return CancellablePromise.resolve(JSON.parse('{"status": 0, "databases": ["default"]}'));
+    }
+    if (options.entry.path.length === 1) {
+      return CancellablePromise.resolve(
         JSON.parse(
           '{"status": 0, "tables_meta": [{"comment": "comment", "type": "Table", "name": "foo"}, {"comment": null, "type": "View", "name": "bar_view"}, {"comment": null, "type": "Table", "name": "bar"}]}'
         )
       );
-    } else if (options.path.length === 2) {
-      deferred.resolve(
+    }
+    if (options.entry.path.length === 2) {
+      return CancellablePromise.resolve(
         JSON.parse(
           '{"status": 0, "support_updates": false, "hdfs_link": "/filebrowser/view=/user/hive/warehouse/customers", "extended_columns": [{"comment": "", "type": "int", "name": "id"}, {"comment": "", "type": "string", "name": "name"}, {"comment": "", "type": "struct<email_format:string,frequency:string,categories:struct<promos:boolean,surveys:boolean>>", "name": "email_preferences"}, {"comment": "", "type": "map<string,struct<street_1:string,street_2:string,city:string,state:string,zip_code:string>>", "name": "addresses"}, {"comment": "", "type": "array<struct<order_id:string,order_date:string,items:array<struct<product_id:int,sku:string,name:string,price:double,qty:int>>>>", "name": "orders"}], "columns": ["id", "name", "email_preferences", "addresses", "orders"], "partition_keys": []}'
         )
       );
-    } else if (options.path.length === 3) {
-      deferred.resolve(
+    }
+    if (options.entry.path.length === 3) {
+      return CancellablePromise.resolve(
         JSON.parse(
           '{"status": 0, "comment": "", "type": "struct", "name": "email_preferences", "fields": [{"type": "string", "name": "email_format"}, {"type": "string", "name": "frequency"}, {"fields": [{"type": "boolean", "name": "promos"}, {"type": "boolean", "name": "surveys"}], "type": "struct", "name": "categories"}]}'
         )
       );
-    } else if (options.path.length > 3) {
-      deferred.resolve(
+    }
+    if (options.entry.path.length > 3) {
+      return CancellablePromise.resolve(
         JSON.parse(
           '{"status": 0, "fields": [{"type": "boolean", "name": "promos"}, {"type": "boolean", "name": "surveys"}], "type": "struct", "name": "categories"}'
         )
       );
-    } else {
-      deferred.reject();
     }
-    return deferred.promise();
+    return CancellablePromise.reject();
   });
 
   jest.spyOn(ApiHelper, 'fetchHdfsPath').mockImplementation(options => {