Sfoglia il codice sorgente

[catalog] Move the partions fetch function to Axios

Johan Ahlen 5 anni fa
parent
commit
add2c39005

+ 49 - 34
desktop/core/src/desktop/js/api/utils.ts

@@ -14,7 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import axios, { AxiosResponse, AxiosTransformer } from 'axios';
+import axios, { AxiosError, AxiosTransformer } from 'axios';
 import qs from 'qs';
 
 import { CancellablePromise } from './cancellablePromise';
@@ -42,47 +42,57 @@ export const successResponseIsError = (responseData?: DefaultApiResponse): boole
 
 const UNKNOWN_ERROR_MESSAGE = 'Unknown error occurred';
 
-export const extractErrorMessage = (errorResponse?: DefaultApiResponse | string): string => {
+export const extractErrorMessage = (
+  errorResponse?: DefaultApiResponse | AxiosError | string
+): string => {
   if (!errorResponse) {
     return UNKNOWN_ERROR_MESSAGE;
   }
   if (typeof errorResponse === 'string') {
     return errorResponse;
   }
-  if (errorResponse.statusText && errorResponse.statusText !== 'abort') {
-    return errorResponse.statusText;
+  const defaultResponse = <DefaultApiResponse>errorResponse;
+  if (defaultResponse.statusText && defaultResponse.statusText !== 'abort') {
+    return defaultResponse.statusText;
   }
-  if (errorResponse.responseText) {
+  if (defaultResponse.responseText) {
     try {
-      const errorJs = JSON.parse(errorResponse.responseText);
+      const errorJs = JSON.parse(defaultResponse.responseText);
       if (errorJs.message) {
         return errorJs.message;
       }
     } catch (err) {}
-    return errorResponse.responseText;
+    return defaultResponse.responseText;
   }
   if (errorResponse.message) {
     return errorResponse.message;
   }
-  if (errorResponse.statusText) {
-    return errorResponse.statusText;
+  if (defaultResponse.statusText) {
+    return defaultResponse.statusText;
   }
-  if (errorResponse.error && typeof errorResponse.error === 'string') {
-    return errorResponse.error;
+  if (defaultResponse.error && typeof defaultResponse.error === 'string') {
+    return defaultResponse.error;
   }
   return UNKNOWN_ERROR_MESSAGE;
 };
 
-export const post = <T, U = unknown>(
+export const post = <T, U = unknown, E = string>(
   url: string,
   data?: U,
   options?: {
     silenceErrors?: boolean;
     ignoreSuccessErrors?: boolean;
     transformResponse?: AxiosTransformer;
-    handleResponse?: (
-      response: T & DefaultApiResponse
-    ) => { valid: boolean; reason?: unknown; adjustedResponse?: T } | undefined;
+    handleSuccess?: (
+      response: T & DefaultApiResponse,
+      resolve: (val: T) => void,
+      reject: (err: unknown) => void
+    ) => void;
+    handleError?: (
+      errorResponse: AxiosError<E>,
+      resolve: (val: T) => void,
+      reject: (err: unknown) => void
+    ) => void;
   }
 ): CancellablePromise<T> =>
   new CancellablePromise((resolve, reject, onCancel) => {
@@ -95,10 +105,10 @@ export const post = <T, U = unknown>(
       }
     };
 
-    const handleErrorResponse = (response: AxiosResponse<DefaultApiResponse>): void => {
-      const errorMessage = extractErrorMessage(response.data);
+    const handleErrorResponse = (err: AxiosError<DefaultApiResponse>): void => {
+      const errorMessage = extractErrorMessage(err.response && err.response.data);
       reject(errorMessage);
-      notifyError(errorMessage, response.data);
+      notifyError(errorMessage, (err && err.response) || err);
     };
 
     const cancelTokenSource = axios.CancelToken.source();
@@ -110,26 +120,31 @@ export const post = <T, U = unknown>(
         transformResponse: options && options.transformResponse
       })
       .then(response => {
-        if (options && options.handleResponse) {
-          const handledResponse = options.handleResponse(response.data);
-          if (handledResponse) {
-            if (handledResponse.valid) {
-              resolve(handledResponse.adjustedResponse || response.data);
-            } else {
-              reject(handledResponse.reason);
-              notifyError(String(handledResponse.reason), response.data);
-            }
-            return;
-          }
-        }
-        if ((!options || !options.ignoreSuccessErrors) && successResponseIsError(response.data)) {
-          handleErrorResponse(response);
+        if (options && options.handleSuccess) {
+          options.handleSuccess(response.data, resolve, reason => {
+            reject(reason);
+            notifyError(String(reason), response.data);
+          });
+        } else if (
+          (!options || !options.ignoreSuccessErrors) &&
+          successResponseIsError(response.data)
+        ) {
+          const errorMessage = extractErrorMessage(response && response.data);
+          reject(errorMessage);
+          notifyError(errorMessage, response);
         } else {
           resolve(response.data);
         }
       })
-      .catch(err => {
-        handleErrorResponse(err);
+      .catch((err: AxiosError) => {
+        if (options && options.handleError) {
+          options.handleError(err, resolve, reason => {
+            handleErrorResponse(err);
+            notifyError(String(reason), err);
+          });
+        } else {
+          handleErrorResponse(err);
+        }
       })
       .finally(() => {
         completed = true;

+ 162 - 227
desktop/core/src/desktop/js/catalog/DataCatalogEntry.ts

@@ -15,7 +15,12 @@
 // limitations under the License.
 
 import { Cancellable, CancellablePromise } from 'api/cancellablePromise';
-import { fetchDescribe, fetchNavigatorMetadata, fetchSourceMetadata } from 'catalog/api';
+import {
+  fetchDescribe,
+  fetchNavigatorMetadata,
+  fetchPartitions,
+  fetchSourceMetadata
+} from 'catalog/api';
 import MultiTableEntry, { TopAggs, TopFilters, TopJoins } from 'catalog/MultiTableEntry';
 import { getOptimizer } from './optimizer/optimizer';
 import * as ko from 'knockout';
@@ -26,7 +31,7 @@ import {
   applyCancellable,
   fetchAndSave,
   FetchOptions,
-  setSilencedErrors
+  forceSilencedErrors
 } from 'catalog/catalogUtils';
 import { Compute, Connector, Namespace } from 'types/config';
 import { hueWindow } from 'types/types';
@@ -126,6 +131,7 @@ export interface FieldSourceMeta extends TimestampedData {
 
 export type SourceMeta = RootSourceMeta | DatabaseSourceMeta | TableSourceMeta | FieldSourceMeta;
 export type FieldSample = string | number | null | undefined;
+type ReloadOptions = Omit<CatalogGetOptions, 'cachedOnly' | 'refreshCache'>;
 
 export interface NavigatorMeta extends TimestampedData {
   clusteredByColNames: unknown;
@@ -195,7 +201,7 @@ export interface Partitions extends TimestampedData {
     notebookUrl: string;
     partitionSpec: string;
     readUrl: string;
-  };
+  }[];
 }
 
 export interface SampleMeta {
@@ -217,131 +223,27 @@ export interface OptimizerMeta extends TimestampedData {
   hueTimestamp?: number;
 }
 
-/**
- * Helper function to reload the source meta for the given entry
- */
-const reloadSourceMeta = (
-  entry: DataCatalogEntry,
-  options?: { silenceErrors?: boolean }
-): CancellablePromise<SourceMeta> => {
-  entry.sourceMetaPromise = new CancellablePromise<SourceMeta>(
-    async (resolve, reject, onCancel) => {
-      onCancel(() => {
-        entry.sourceMetaPromise = undefined;
-      });
-
-      if (entry.dataCatalog.invalidatePromise) {
-        try {
-          await entry.dataCatalog.invalidatePromise;
-        } catch (err) {}
-      }
-
-      try {
-        entry.sourceMeta = await fetchSourceMetadata({
-          ...options,
-          entry
-        });
-        resolve(entry.sourceMeta);
-      } catch (err) {
-        reject(err);
-        return;
-      }
-      entry.saveLater();
-    }
-  );
-  return entry.sourceMetaPromise;
-};
-
-/**
- * Helper function to reload the navigator meta for the given entry
- */
-const reloadNavigatorMeta = (
+const setAndSave = async <T extends keyof DataCatalogEntry>(
   entry: DataCatalogEntry,
-  options?: { silenceErrors?: boolean }
-): CancellablePromise<NavigatorMeta> => {
-  if (entry.canHaveNavigatorMetadata()) {
-    entry.navigatorMetaPromise = new CancellablePromise<NavigatorMeta>(
-      async (resolve, reject, onCancel) => {
-        onCancel(() => {
-          entry.navigatorMetaPromise = undefined;
-        });
-        try {
-          entry.navigatorMeta = await fetchNavigatorMetadata({ ...options, entry });
-          resolve(entry.navigatorMeta);
-          if (entry.commentObservable) {
-            entry.commentObservable(entry.getResolvedComment());
-          }
-        } catch (err) {
-          reject(err);
-          return;
-        }
-        entry.saveLater();
-      }
-    );
-  } else {
-    entry.navigatorMetaPromise = CancellablePromise.reject();
+  attribute: T,
+  promise: Promise<DataCatalogEntry[T]>,
+  resolve: (val: DataCatalogEntry[T]) => void,
+  reject: (reason: unknown) => void
+): Promise<void> => {
+  try {
+    entry[attribute] = await promise;
+    resolve(entry[attribute]);
+  } catch (err) {
+    reject(err || 'Fetch failed');
+    return;
   }
-  return entry.navigatorMetaPromise;
+  entry.saveLater();
 };
 
-/**
- * Helper function to reload the analysis for the given entry
- */
-const reloadAnalysis = (
-  entry: DataCatalogEntry,
-  options?: { silenceErrors?: boolean; refreshAnalysis?: boolean }
-): CancellablePromise<Analysis> => {
-  entry.analysisPromise = new CancellablePromise<Analysis>(async (resolve, reject, onCancel) => {
-    const fetchPromise = fetchDescribe({
-      entry,
-      ...options
-    });
-
-    onCancel(() => {
-      fetchPromise.cancel();
-      entry.analysisPromise = undefined;
-    });
-
-    try {
-      entry.analysis = await fetchPromise;
-      resolve(entry.analysis);
-    } catch (err) {
-      reject(err || 'Fetch failed');
-      return;
-    }
-    entry.saveLater();
-  });
-  return entry.analysisPromise;
-};
-
-/**
- * Helper function to reload the partitions for the given entry
- */
-const reloadPartitions = (
-  entry: DataCatalogEntry,
-  apiOptions?: { silenceErrors?: boolean }
-): CancellablePromise<Partitions> => {
-  entry.partitionsPromise = new CancellablePromise<Partitions>((resolve, reject, onCancel) => {
-    const fetchPromise = fetchAndSave(
-      (<(options: FetchOptions) => CancellableJqPromise<Partitions>>(
-        (<unknown>apiHelper.fetchPartitions)
-      )).bind(apiHelper),
-      val => {
-        entry.partitions = val;
-      },
-      entry,
-      apiOptions
-    );
-
-    onCancel(() => {
-      fetchPromise.cancel();
-      entry.partitionsPromise = undefined;
-    });
+const cachedOnly = (options?: CatalogGetOptions): boolean => !!(options && options.cachedOnly);
 
-    fetchPromise.then(resolve).fail(reject);
-  });
-  return entry.partitionsPromise;
-};
+const shouldReload = (options?: CatalogGetOptions & { refreshAnalysis?: boolean }): boolean =>
+  !!(!DataCatalog.cacheEnabled() || (options && (options.refreshCache || options.refreshAnalysis)));
 
 /**
  * Helper function to reload the sample for the given entry
@@ -554,6 +456,97 @@ export default class DataCatalogEntry {
     });
   }
 
+  private reloadAnalysis(
+    options?: ReloadOptions & { refreshAnalysis?: boolean }
+  ): CancellablePromise<Analysis> {
+    this.analysisPromise = new CancellablePromise<Analysis>(async (resolve, reject, onCancel) => {
+      const fetchPromise = fetchDescribe({
+        entry: this,
+        ...options
+      });
+
+      onCancel(() => {
+        fetchPromise.cancel();
+        this.analysisPromise = undefined;
+      });
+
+      await setAndSave(this, 'analysis', fetchPromise, resolve, reject);
+    });
+    return applyCancellable(this.analysisPromise, options);
+  }
+
+  private reloadNavigatorMeta(options?: ReloadOptions): CancellablePromise<NavigatorMeta> {
+    if (this.canHaveNavigatorMetadata()) {
+      this.navigatorMetaPromise = new CancellablePromise<NavigatorMeta>(
+        async (resolve, reject, onCancel) => {
+          onCancel(() => {
+            this.navigatorMetaPromise = undefined;
+          });
+          await setAndSave(
+            this,
+            'navigatorMeta',
+            fetchNavigatorMetadata({ ...options, entry: this }),
+            resolve,
+            reject
+          );
+          if (this.commentObservable) {
+            this.commentObservable(this.getResolvedComment());
+          }
+        }
+      );
+    } else {
+      this.navigatorMetaPromise = CancellablePromise.reject();
+    }
+    return applyCancellable(this.navigatorMetaPromise);
+  }
+
+  private reloadPartitions(options?: ReloadOptions): CancellablePromise<Partitions> {
+    this.partitionsPromise = new CancellablePromise<Partitions>(
+      async (resolve, reject, onCancel) => {
+        onCancel(() => {
+          this.partitionsPromise = undefined;
+        });
+
+        await setAndSave(
+          this,
+          'partitions',
+          fetchPartitions({ ...options, entry: this }),
+          resolve,
+          reject
+        );
+      }
+    );
+    return applyCancellable(this.partitionsPromise, options);
+  }
+
+  private reloadSourceMeta(options?: ReloadOptions): CancellablePromise<SourceMeta> {
+    this.sourceMetaPromise = new CancellablePromise<SourceMeta>(
+      async (resolve, reject, onCancel) => {
+        onCancel(() => {
+          this.sourceMetaPromise = undefined;
+        });
+
+        if (this.dataCatalog.invalidatePromise) {
+          try {
+            await this.dataCatalog.invalidatePromise;
+          } catch (err) {}
+        }
+
+        await setAndSave(
+          this,
+          'sourceMeta',
+          fetchSourceMetadata({
+            ...options,
+            entry: this
+          }),
+          resolve,
+          reject
+        );
+      }
+    );
+    return applyCancellable(this.sourceMetaPromise, options);
+  }
+
   /**
    * Save the entry to cache
    */
@@ -595,12 +588,12 @@ export default class DataCatalogEntry {
    * Get the children of the catalog entry, columns for a table entry etc.
    */
   getChildren(options?: CatalogGetOptions): CancellablePromise<DataCatalogEntry[]> {
-    if (this.childrenPromise && DataCatalog.cacheEnabled() && (!options || !options.refreshCache)) {
-      return applyCancellable(this.childrenPromise, options);
+    if (!this.childrenPromise && cachedOnly(options)) {
+      return CancellablePromise.reject();
     }
 
-    if (!this.childrenPromise && options && options.cachedOnly) {
-      return CancellablePromise.reject(false);
+    if (this.childrenPromise && !shouldReload(options)) {
+      return applyCancellable(this.childrenPromise, options);
     }
 
     this.childrenPromise = new CancellablePromise<DataCatalogEntry[]>(
@@ -739,17 +732,13 @@ export default class DataCatalogEntry {
   loadNavigatorMetaForChildren(
     options?: Omit<CatalogGetOptions, 'cachedOnly'>
   ): CancellablePromise<DataCatalogEntry[]> {
-    options = setSilencedErrors(options);
+    options = forceSilencedErrors(options);
 
     if (!this.canHaveNavigatorMetadata() || this.isField()) {
       return CancellablePromise.resolve([]);
     }
 
-    if (
-      this.navigatorMetaForChildrenPromise &&
-      DataCatalog.cacheEnabled() &&
-      (!options || !options.refreshCache)
-    ) {
+    if (this.navigatorMetaForChildrenPromise && !shouldReload(options)) {
       return applyCancellable(this.navigatorMetaForChildrenPromise, options);
     }
 
@@ -770,11 +759,7 @@ export default class DataCatalogEntry {
 
           const someHaveNavMeta = children.some(childEntry => childEntry.navigatorMeta);
 
-          if (
-            someHaveNavMeta &&
-            DataCatalog.cacheEnabled() &&
-            (!options || !options.refreshCache)
-          ) {
+          if (someHaveNavMeta && !shouldReload(options)) {
             resolve(children);
             return;
           }
@@ -929,26 +914,17 @@ export default class DataCatalogEntry {
   loadOptimizerPopularityForChildren(
     options?: CatalogGetOptions
   ): CancellablePromise<DataCatalogEntry[]> {
-    options = setSilencedErrors(options);
+    options = forceSilencedErrors(options);
 
     if (!this.dataCatalog.canHaveOptimizerMeta()) {
       return CancellablePromise.reject();
     }
 
-    if (
-      this.optimizerPopularityForChildrenPromise &&
-      DataCatalog.cacheEnabled() &&
-      (!options || !options.refreshCache)
-    ) {
+    if (this.optimizerPopularityForChildrenPromise && !shouldReload(options)) {
       return applyCancellable(this.optimizerPopularityForChildrenPromise, options);
     }
 
-    if (
-      this.definition &&
-      this.definition.optimizerLoaded &&
-      DataCatalog.cacheEnabled() &&
-      (!options || !options.refreshCache)
-    ) {
+    if (this.definition && this.definition.optimizerLoaded && !shouldReload(options)) {
       this.optimizerPopularityForChildrenPromise = new CancellablePromise<DataCatalogEntry[]>(
         async (resolve, reject, onCancel) => {
           const childPromise = this.getChildren(options);
@@ -972,8 +948,7 @@ export default class DataCatalogEntry {
           });
 
           const popularityPromise = getOptimizer(this.dataCatalog.connector).fetchPopularity({
-            silenceErrors: options && options.silenceErrors,
-            refreshCache: options && options.refreshCache,
+            ...options,
             paths: [this.path]
           });
           cancellablePromises.push(popularityPromise);
@@ -1127,20 +1102,13 @@ export default class DataCatalogEntry {
 
   /**
    * Sets the comment in the proper source
-   *
-   * @param {string} comment
-   * @param {Object} [apiOptions]
-   * @param {boolean} [apiOptions.silenceErrors]
-   * @param {boolean} [apiOptions.refreshCache]
-   *
-   * @return {Promise}
    */
   async setComment(
     comment: string,
-    apiOptions?: Omit<CatalogGetOptions, 'cachedOnly' | 'cancellable'>
+    options?: Omit<CatalogGetOptions, 'cachedOnly' | 'cancellable'>
   ): Promise<string> {
     if (this.canHaveNavigatorMetadata()) {
-      const navigatorMeta = await this.getNavigatorMeta(apiOptions);
+      const navigatorMeta = await this.getNavigatorMeta(options);
       if (!navigatorMeta) {
         throw new Error('Could not load navigator metadata.');
       }
@@ -1159,7 +1127,7 @@ export default class DataCatalogEntry {
               this.navigatorMetaPromise = CancellablePromise.resolve(entity);
               this.saveLater();
             }
-            this.getComment(apiOptions)
+            this.getComment(options)
               .then(comment => {
                 if (this.commentObservable) {
                   this.commentObservable(comment);
@@ -1183,10 +1151,8 @@ export default class DataCatalogEntry {
         })
         .done(async () => {
           try {
-            await reloadSourceMeta(this, {
-              silenceErrors: apiOptions && apiOptions.silenceErrors
-            });
-            const comment = await this.getComment(apiOptions);
+            await this.reloadSourceMeta(options);
+            const comment = await this.getComment(options);
             if (this.commentObservable) {
               this.commentObservable(comment);
             }
@@ -1554,16 +1520,13 @@ export default class DataCatalogEntry {
    * Gets the source metadata for the entry. It will fetch it if not cached or if the refresh option is set.
    */
   getSourceMeta(options?: CatalogGetOptions): CancellablePromise<SourceMeta> {
-    if (options && options.cachedOnly) {
-      return (
-        (this.sourceMetaPromise && applyCancellable(this.sourceMetaPromise, options)) ||
-        CancellablePromise.reject(false)
-      );
+    if (!this.sourceMetaPromise && cachedOnly(options)) {
+      return CancellablePromise.reject();
     }
-    if (options && options.refreshCache) {
-      return applyCancellable(reloadSourceMeta(this, options));
+    if (!this.sourceMetaPromise || shouldReload(options)) {
+      return this.reloadSourceMeta(options);
     }
-    return applyCancellable(this.sourceMetaPromise || reloadSourceMeta(this, options), options);
+    return applyCancellable(this.sourceMetaPromise, options);
   }
 
   /**
@@ -1574,83 +1537,58 @@ export default class DataCatalogEntry {
       refreshAnalysis?: boolean;
     }
   ): CancellablePromise<Analysis> {
-    if (options && options.cachedOnly) {
-      return (
-        (this.analysisPromise && applyCancellable(this.analysisPromise, options)) ||
-        CancellablePromise.reject(false)
-      );
+    if (!this.analysisPromise && cachedOnly(options)) {
+      return CancellablePromise.reject();
     }
-    if (options && (options.refreshCache || options.refreshAnalysis)) {
-      return applyCancellable(reloadAnalysis(this, options), options);
+    if (!this.analysisPromise || shouldReload(options)) {
+      return this.reloadAnalysis(options);
     }
-    return applyCancellable(this.analysisPromise || reloadAnalysis(this, options), options);
+    return applyCancellable(this.analysisPromise, options);
   }
 
   /**
    * Gets the partitions for the entry. It will fetch it if not cached or if the refresh option is set.
    */
   getPartitions(options?: CatalogGetOptions): CancellablePromise<Partitions> {
-    if (!this.isTableOrView()) {
+    if (!this.isTableOrView() || (!this.partitionsPromise && cachedOnly(options))) {
       return CancellablePromise.reject();
     }
-    if (options && options.cachedOnly) {
-      return (
-        (this.partitionsPromise && applyCancellable(this.partitionsPromise, options)) ||
-        CancellablePromise.reject()
-      );
-    }
-    if (options && options.refreshCache) {
-      return applyCancellable(reloadPartitions(this, options), options);
+    if (!this.partitionsPromise || shouldReload(options)) {
+      return this.reloadPartitions(options);
     }
-    return applyCancellable(this.partitionsPromise || reloadPartitions(this, options), options);
+    return applyCancellable(this.partitionsPromise, options);
   }
 
   /**
    * Gets the Navigator metadata for the entry. It will fetch it if not cached or if the refresh option is set.
    */
   getNavigatorMeta(options?: CatalogGetOptions): CancellablePromise<NavigatorMeta> {
-    options = setSilencedErrors(options);
-
-    if (
-      !this.canHaveNavigatorMetadata() ||
-      (!this.navigatorMetaPromise && options && options.cachedOnly)
-    ) {
+    options = forceSilencedErrors(options);
+    if (!this.canHaveNavigatorMetadata() || (!this.navigatorMetaPromise && cachedOnly(options))) {
       return CancellablePromise.reject();
     }
-
-    if (
-      this.navigatorMetaPromise &&
-      DataCatalog.cacheEnabled() &&
-      (!options || !options.refreshCache)
-    ) {
-      return applyCancellable(this.navigatorMetaPromise, options);
+    if (!this.navigatorMetaPromise || shouldReload(options)) {
+      return this.reloadNavigatorMeta(options);
     }
-
-    return applyCancellable(reloadNavigatorMeta(this, options), options);
+    return applyCancellable(this.navigatorMetaPromise, options);
   }
 
   /**
    * Gets the Nav Opt metadata for the entry. It will fetch it if not cached or if the refresh option is set.
    */
   getOptimizerMeta(options?: CatalogGetOptions): CancellablePromise<OptimizerMeta> {
-    options = setSilencedErrors(options);
+    options = forceSilencedErrors(options);
 
     if (!this.dataCatalog.canHaveOptimizerMeta() || !this.isTableOrView()) {
       return CancellablePromise.reject();
     }
-    if (options && options.cachedOnly) {
-      return (
-        (this.optimizerMetaPromise && applyCancellable(this.optimizerMetaPromise, options)) ||
-        CancellablePromise.reject()
-      );
+    if (!this.optimizerMetaPromise && cachedOnly(options)) {
+      return CancellablePromise.reject();
     }
-    if (options && options.refreshCache) {
+    if (!this.optimizerMetaPromise || shouldReload(options)) {
       return applyCancellable(reloadOptimizerMeta(this, options), options);
     }
-    return applyCancellable(
-      this.optimizerMetaPromise || reloadOptimizerMeta(this, options),
-      options
-    );
+    return applyCancellable(this.optimizerMetaPromise, options);
   }
 
   /**
@@ -1671,7 +1609,7 @@ export default class DataCatalogEntry {
           compute: this.compute,
           path: this.path,
           silenceErrors: !!(options && options.silenceErrors),
-          operation: operation
+          operation
         });
         onCancel(() => {
           fetchPromise.cancel();
@@ -1730,7 +1668,7 @@ export default class DataCatalogEntry {
           }
         } catch (err) {}
 
-        if (options && options.cachedOnly) {
+        if (cachedOnly(options)) {
           reject();
         } else {
           const reloadPromise = applyCancellable(reloadSample(this, options), options);
@@ -1746,16 +1684,13 @@ export default class DataCatalogEntry {
       return applyCancellable(samplePromise, options);
     }
 
-    if (options && options.cachedOnly) {
-      return (
-        (this.samplePromise && applyCancellable(this.samplePromise, options)) ||
-        CancellablePromise.reject()
-      );
+    if (!this.samplePromise && cachedOnly(options)) {
+      return CancellablePromise.reject();
     }
-    if (options && options.refreshCache) {
+    if (!this.samplePromise || shouldReload(options)) {
       return applyCancellable(reloadSample(this, options), options);
     }
-    return applyCancellable(this.samplePromise || reloadSample(this, options), options);
+    return applyCancellable(this.samplePromise, options);
   }
 
   /**

+ 70 - 21
desktop/core/src/desktop/js/catalog/api.ts

@@ -16,7 +16,12 @@
 
 import { CancellablePromise } from 'api/cancellablePromise';
 import { DefaultApiResponse, extractErrorMessage, post, successResponseIsError } from 'api/utils';
-import DataCatalogEntry, { Analysis, NavigatorMeta, SourceMeta } from 'catalog/DataCatalogEntry';
+import DataCatalogEntry, {
+  Analysis,
+  NavigatorMeta,
+  Partitions,
+  SourceMeta
+} from 'catalog/DataCatalogEntry';
 import { sleep } from 'utils/hueUtils';
 
 interface AnalyzeResponse {
@@ -33,6 +38,10 @@ interface SharedFetchOptions {
 const AUTOCOMPLETE_URL_PREFIX = '/notebook/api/autocomplete/';
 const DESCRIBE_URL = '/notebook/api/describe/';
 const FIND_ENTITY_URL = '/metadata/api/catalog/find_entity';
+const METASTORE_TABLE_URL_PREFIX = '/metastore/table/';
+
+const getEntryUrlPath = (entry: DataCatalogEntry) =>
+  entry.path.join('/') + (entry.path.length ? '/' : '');
 
 const performAnalyze = ({
   entry,
@@ -64,7 +73,7 @@ const performAnalyze = ({
       const analyzeResponse = await post<DefaultApiResponse & { watch_url?: string }>(
         `/${
           entry.getConnector().id === 'hive' ? 'beeswax' : entry.getConnector().id
-        }/api/analyze/${entry.path.join('/')}/`,
+        }/api/analyze/${getEntryUrlPath(entry)}`,
         undefined,
         { silenceErrors }
       );
@@ -122,13 +131,14 @@ export const fetchDescribe = ({
       },
       {
         silenceErrors,
-        handleResponse: (response: Analysis & DefaultApiResponse) => {
+        handleSuccess: (response: Analysis & DefaultApiResponse, postResolve, postReject) => {
           if (successResponseIsError(response)) {
-            return { valid: false, reason: extractErrorMessage(response) };
+            postReject(extractErrorMessage(response));
+          } else {
+            const adjustedResponse = response;
+            adjustedResponse.hueTimestamp = Date.now();
+            postResolve(adjustedResponse);
           }
-          const adjustedResponse = response;
-          adjustedResponse.hueTimestamp = Date.now();
-          return { valid: true, adjustedResponse };
         }
       }
     );
@@ -173,30 +183,68 @@ export const fetchNavigatorMetadata = ({
     },
     {
       silenceErrors,
-      handleResponse: (
-        response: (NavigatorMeta | { entity: NavigatorMeta }) & DefaultApiResponse
+      handleSuccess: (
+        response: (NavigatorMeta | { entity: NavigatorMeta }) & DefaultApiResponse,
+        resolve,
+        reject
       ) => {
         if (successResponseIsError(response)) {
-          return {
-            valid: false,
-            reason: `Navigator meta failed: ${extractErrorMessage(response)}`
-          };
+          reject(extractErrorMessage(response));
+        } else {
+          const adjustedResponse = (<{ entity: NavigatorMeta }>response).entity || response;
+          adjustedResponse.hueTimestamp = Date.now();
+          resolve(adjustedResponse);
         }
-        const adjustedResponse = (<{ entity: NavigatorMeta }>response).entity || response;
-        adjustedResponse.hueTimestamp = Date.now();
-
-        return { valid: true, adjustedResponse };
       }
     }
   );
 };
 
+export const fetchPartitions = ({
+  entry,
+  silenceErrors
+}: SharedFetchOptions): CancellablePromise<Partitions> =>
+  post<Partitions>(
+    `${METASTORE_TABLE_URL_PREFIX}${getEntryUrlPath(entry)}partitions`,
+    {
+      format: 'json',
+      cluster: (entry.compute && JSON.stringify(entry.compute)) || '""'
+    },
+    {
+      silenceErrors,
+      handleSuccess: (response, resolve, reject) => {
+        const adjustedResponse = response || {};
+        adjustedResponse.hueTimestamp = Date.now();
+        if (successResponseIsError(response)) {
+          reject(`Partitions failed: ${extractErrorMessage(response)}`);
+        } else {
+          resolve(adjustedResponse);
+        }
+      },
+      handleError: (errorResponse, resolve, reject) => {
+        if (
+          errorResponse.response &&
+          errorResponse.response.data &&
+          errorResponse.response.data.indexOf('is not partitioned') !== -1
+        ) {
+          resolve({
+            hueTimestamp: Date.now(),
+            partition_keys_json: [],
+            partition_values_json: []
+          });
+        } else {
+          reject(errorResponse);
+        }
+      }
+    }
+  );
+
 export const fetchSourceMetadata = ({
   entry,
   silenceErrors
 }: SharedFetchOptions): CancellablePromise<SourceMeta> =>
   post<SourceMeta>(
-    `${AUTOCOMPLETE_URL_PREFIX}${entry.path.join('/')}${entry.path.length ? '/' : ''}`,
+    `${AUTOCOMPLETE_URL_PREFIX}${getEntryUrlPath(entry)}`,
     {
       notebook: {},
       snippet: JSON.stringify({
@@ -207,7 +255,7 @@ export const fetchSourceMetadata = ({
     },
     {
       silenceErrors,
-      handleResponse: response => {
+      handleSuccess: (response, resolve, reject) => {
         const message = <string>response.error || response.message || '';
         const adjustedResponse = response || {};
         adjustedResponse.notFound =
@@ -219,9 +267,10 @@ export const fetchSourceMetadata = ({
         adjustedResponse.hueTimestamp = Date.now();
 
         if (!adjustedResponse.notFound && successResponseIsError(response)) {
-          return { valid: false, reason: `Source meta failed: ${extractErrorMessage(response)}` };
+          reject(extractErrorMessage(response));
+        } else {
+          resolve(adjustedResponse);
         }
-        return { valid: true, adjustedResponse };
       }
     }
   );

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

@@ -16,6 +16,7 @@
 
 import CancellableJqPromise from 'api/cancellableJqPromise';
 import { CancellablePromise } from 'api/cancellablePromise';
+import { CatalogGetOptions } from 'catalog/dataCatalog';
 import DataCatalogEntry from 'catalog/DataCatalogEntry';
 import MultiTableEntry from 'catalog/MultiTableEntry';
 import { Compute, Connector } from 'types/config';
@@ -58,7 +59,7 @@ export const fetchAndSave = <T>(
 /**
  * Helper function that adds sets the silence errors option to true if not specified
  */
-export const setSilencedErrors = (options?: {
+export const forceSilencedErrors = (options?: {
   silenceErrors?: boolean;
 }): { silenceErrors?: boolean } => {
   if (!options) {
@@ -75,7 +76,7 @@ export const setSilencedErrors = (options?: {
  */
 export const applyCancellable = <T>(
   promise: CancellablePromise<T>,
-  options?: { cancellable?: boolean }
+  options?: Pick<CatalogGetOptions, 'cancellable'>
 ): CancellablePromise<T> => {
   if (promise && promise.preventCancel && (!options || !options.cancellable)) {
     promise.preventCancel();