Browse Source

[frontend] Simplify the context catalog and switch to Axios

Johan Åhlén 4 years ago
parent
commit
a6762c4a4e
25 changed files with 452 additions and 543 deletions
  1. 4 2
      apps/oozie/src/oozie/static/oozie/js/workflow-editor.ko.js
  2. 2 2
      apps/pig/src/pig/templates/app.mako
  3. 0 33
      desktop/core/src/desktop/js/api/apiHelper.js
  4. 1 1
      desktop/core/src/desktop/js/api/utils.ts
  5. 16 0
      desktop/core/src/desktop/js/apps/editor/execution/events.ts
  6. 7 7
      desktop/core/src/desktop/js/apps/tableBrowser/metastoreSource.js
  7. 32 1
      desktop/core/src/desktop/js/catalog/api.ts
  8. 0 371
      desktop/core/src/desktop/js/catalog/contextCatalog.js
  9. 281 0
      desktop/core/src/desktop/js/catalog/contextCatalog.ts
  10. 22 0
      desktop/core/src/desktop/js/catalog/events.ts
  11. 2 3
      desktop/core/src/desktop/js/catalog/optimizer/SqlAnalyzer.ts
  12. 16 0
      desktop/core/src/desktop/js/config/events.ts
  13. 0 63
      desktop/core/src/desktop/js/config/hueConfig.d.ts
  14. 1 1
      desktop/core/src/desktop/js/config/types.ts
  15. 1 0
      desktop/core/src/desktop/js/hue.js
  16. 10 8
      desktop/core/src/desktop/js/jquery/plugins/jquery.hiveautocomplete.js
  17. 14 14
      desktop/core/src/desktop/js/ko/components/assist/assistDbSource.js
  18. 21 19
      desktop/core/src/desktop/js/ko/components/contextPopover/ko.contextPopover.js
  19. 7 8
      desktop/core/src/desktop/js/ko/components/ko.contextSelector.js
  20. 2 2
      desktop/core/src/desktop/static/desktop/js/jquery.hiveautocomplete.js
  21. 5 0
      desktop/core/src/desktop/templates/hue.mako
  22. 2 2
      desktop/libs/dashboard/src/dashboard/static/dashboard/js/search.ko.js
  23. 2 2
      desktop/libs/indexer/src/indexer/templates/importer.mako
  24. 2 2
      desktop/libs/indexer/src/indexer/templates/indexes.mako
  25. 2 2
      desktop/libs/indexer/src/indexer/templates/topics.mako

+ 4 - 2
apps/oozie/src/oozie/static/oozie/js/workflow-editor.ko.js

@@ -541,8 +541,10 @@ var WorkflowEditorViewModel = function (layout_json, workflow_json, credentials_
   self.availableComputes = ko.observableArray();
   self.compute = ko.observable();
 
-  contextCatalog.getNamespaces({ connector: { id: 'oozie' } }).done(function (context) { self.availableNamespaces(context.namespaces) });
-  contextCatalog.getComputes({ connector: { id: 'oozie' } }).done(self.availableComputes);
+  contextCatalog.getNamespaces({ connector: { id: 'oozie' } })
+      .then(function (context) { self.availableNamespaces(context.namespaces);}).catch();
+  contextCatalog.getComputes({ connector: { id: 'oozie' } })
+      .then(self.availableComputes).catch();
 
 
   self.previewColumns = ko.observable("");

+ 2 - 2
apps/pig/src/pig/templates/app.mako

@@ -1021,7 +1021,7 @@ ${ commonshare() | n,unicode }
     % if autocomplete_base_url != '':
       var apiHelper = window.apiHelper;
       var connector = { id: 'hive' };
-      contextCatalog.getNamespaces({ connector: connector }).done(function (context) {
+      contextCatalog.getNamespaces({ connector: connector }).then(function (context) {
         // TODO: Namespace and compute selection
         dataCatalog.getChildren({
           namespace: context.namespaces[0],
@@ -1032,7 +1032,7 @@ ${ commonshare() | n,unicode }
         }).then(function (childEntries) {
           availableTables = $.map(childEntries, function (entry) { return entry.name }).join(' ');
         }).catch(function() {});
-      });
+      }).catch();
     % endif
 
     function showHiveAutocomplete(databaseName) {

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

@@ -1642,39 +1642,6 @@ class ApiHelper {
     return new CancellableJqPromise(deferred, request);
   }
 
-  /**
-   * @param {Object} options
-   * @param {boolean} [options.silenceErrors]
-   * @param {Connector} options.connector
-   * @return {Promise}
-   */
-  fetchContextNamespaces(options) {
-    const url = '/desktop/api2/context/namespaces/' + options.connector.id;
-    return simpleGet(url, undefined, options);
-  }
-
-  /**
-   * @param {Object} options
-   * @param {boolean} [options.silenceErrors]
-   * @param {Connector} options.connector
-   * @return {Promise}
-   */
-  fetchContextComputes(options) {
-    const url = '/desktop/api2/context/computes/' + options.connector.id;
-    return simpleGet(url, undefined, options);
-  }
-
-  /**
-   * @param {Object} options
-   * @param {boolean} [options.silenceErrors]
-   * @param {Connector} options.connector
-   * @return {Promise}
-   */
-  fetchContextClusters(options) {
-    const url = '/desktop/api2/context/clusters/' + options.connector.id;
-    return simpleGet(url, undefined, options);
-  }
-
   async fetchHueConfigAsync(options) {
     return new Promise((resolve, reject) => {
       $.get(URLS.GET_HUE_CONFIG_API)

+ 1 - 1
desktop/core/src/desktop/js/api/utils.ts

@@ -187,7 +187,7 @@ export const post = <T, U = unknown, E = string>(
 
 export const get = <T, U = unknown>(
   url: string,
-  data: U,
+  data?: U,
   options?: ApiFetchOptions<T>
 ): CancellablePromise<T> =>
   new CancellablePromise((resolve, reject, onCancel) => {

+ 16 - 0
desktop/core/src/desktop/js/apps/editor/execution/events.ts

@@ -1,3 +1,19 @@
+// 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 { Session } from 'apps/editor/execution/api';
 import ExecutionResult from './executionResult';
 import Executable, { ExecutionStatus } from './executable';

+ 7 - 7
desktop/core/src/desktop/js/apps/tableBrowser/metastoreSource.js

@@ -17,7 +17,8 @@
 import $ from 'jquery';
 import * as ko from 'knockout';
 
-import contextCatalog, { NAMESPACES_REFRESHED_EVENT } from 'catalog/contextCatalog';
+import { getNamespaces } from 'catalog/contextCatalog';
+import { NAMESPACES_REFRESHED_TOPIC } from 'catalog/events';
 import huePubSub from 'utils/huePubSub';
 import MetastoreNamespace from 'apps/tableBrowser/metastoreNamespace';
 import {
@@ -131,7 +132,7 @@ class MetastoreSource {
         });
     };
 
-    huePubSub.subscribe(NAMESPACES_REFRESHED_EVENT, connectorId => {
+    huePubSub.subscribe(NAMESPACES_REFRESHED_TOPIC, connectorId => {
       if (this.type !== connectorId) {
         return;
       }
@@ -185,9 +186,8 @@ class MetastoreSource {
 
   loadNamespaces() {
     this.loading(true);
-    contextCatalog
-      .getNamespaces({ connector: this.connector() })
-      .done(context => {
+    getNamespaces({ connector: this.connector() })
+      .then(context => {
         const namespacesWithComputes = context.namespaces.filter(
           namespace => namespace.computes.length
         );
@@ -206,8 +206,8 @@ class MetastoreSource {
         this.namespace(this.namespaces()[0]);
         this.lastLoadNamespacesDeferred.resolve();
       })
-      .fail(this.lastLoadNamespacesDeferred.reject)
-      .always(() => {
+      .catch(this.lastLoadNamespacesDeferred.reject)
+      .finally(() => {
         this.loading(false);
       });
     return this.lastLoadNamespacesDeferred;

+ 32 - 1
desktop/core/src/desktop/js/catalog/api.ts

@@ -15,7 +15,13 @@
 // limitations under the License.
 
 import { Cancellable, CancellablePromise } from 'api/cancellablePromise';
-import { DefaultApiResponse, extractErrorMessage, post, successResponseIsError } from 'api/utils';
+import {
+  DefaultApiResponse,
+  extractErrorMessage,
+  get,
+  post,
+  successResponseIsError
+} from 'api/utils';
 import { closeSession, ExecutionHandle } from 'apps/editor/execution/api';
 import DataCatalogEntry, {
   Analysis,
@@ -28,6 +34,7 @@ import DataCatalogEntry, {
 } from 'catalog/DataCatalogEntry';
 import { hueWindow } from 'types/types';
 import { sleep, UUID } from 'utils/hueUtils';
+import { Cluster, Compute, Connector, Namespace } from '../config/types';
 
 interface AnalyzeResponse {
   status: number;
@@ -167,6 +174,30 @@ export const fetchDescribe = ({
     }
   });
 
+export const fetchClusters = (
+  connector: Connector,
+  silenceErrors?: boolean
+): CancellablePromise<Record<string, Cluster[]>> =>
+  get(`/desktop/api2/context/clusters/${connector.id}`, undefined, {
+    silenceErrors
+  });
+
+export const fetchComputes = (
+  connector: Connector,
+  silenceErrors?: boolean
+): CancellablePromise<Record<string, Compute[]>> =>
+  get(`/desktop/api2/context/computes/${connector.id}`, undefined, {
+    silenceErrors
+  });
+
+export const fetchNamespaces = (
+  connector: Connector,
+  silenceErrors?: boolean
+): CancellablePromise<Record<string, Namespace[]> & { dynamicClusters?: boolean }> =>
+  get(`/desktop/api2/context/namespaces/${connector.id}`, undefined, {
+    silenceErrors
+  });
+
 export const fetchNavigatorMetadata = ({
   entry,
   silenceErrors

+ 0 - 371
desktop/core/src/desktop/js/catalog/contextCatalog.js

@@ -1,371 +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 localforage from 'localforage';
-
-import apiHelper from 'api/apiHelper';
-import huePubSub from 'utils/huePubSub';
-
-export const REFRESH_CONTEXT_CATALOG_EVENT = 'context.catalog.refresh';
-export const CONTEXT_CATALOG_REFRESHED_EVENT = 'context.catalog.refreshed';
-export const NAMESPACES_REFRESHED_EVENT = 'context.catalog.namespaces.refreshed';
-
-/**
- * @typedef {Object} ContextCompute
- * @property {string} id
- * @property {string} name
- */
-
-/**
- * @typedef {Object} ContextNamespace
- * @property {string} id
- * @property {string} name
- * @property {ContextCompute} computes
- */
-
-const STORAGE_POSTFIX = window.LOGGED_USERNAME;
-const CONTEXT_CATALOG_VERSION = 4;
-const NAMESPACES_CONTEXT_TYPE = 'namespaces';
-const DISABLE_CACHE = true;
-
-class ContextCatalog {
-  constructor() {
-    const self = this;
-    self.namespaces = {};
-    self.namespacePromises = {};
-
-    self.computes = {};
-    self.computePromises = {};
-
-    self.clusters = {};
-    self.clusterPromises = {};
-
-    const addPubSubs = () => {
-      if (typeof huePubSub !== 'undefined') {
-        huePubSub.subscribe(REFRESH_CONTEXT_CATALOG_EVENT, () => {
-          const namespacesToRefresh = Object.keys(self.namespaces);
-          self.namespaces = {};
-          self.namespacePromises = {};
-
-          self.computes = {};
-          self.computePromises = {};
-
-          self.clusters = {};
-          self.clusterPromises = {};
-          huePubSub.publish(CONTEXT_CATALOG_REFRESHED_EVENT);
-          namespacesToRefresh.forEach(connectorType => {
-            huePubSub.publish(NAMESPACES_REFRESHED_EVENT, connectorType);
-          });
-        });
-      } else {
-        window.setTimeout(addPubSubs, 100);
-      }
-    };
-
-    addPubSubs();
-  }
-
-  getStore() {
-    if (!self.store) {
-      self.store = localforage.createInstance({
-        name: 'HueContextCatalog_' + STORAGE_POSTFIX
-      });
-    }
-    return self.store;
-  }
-
-  saveLater(contextType, connectorType, entry) {
-    const self = this;
-    window.setTimeout(() => {
-      self.getStore().setItem(connectorType + '_' + contextType, {
-        version: CONTEXT_CATALOG_VERSION,
-        entry: entry
-      });
-    }, 1000);
-  }
-
-  getSaved(contextType, connectorType) {
-    const self = this;
-    const deferred = $.Deferred();
-
-    if (DISABLE_CACHE) {
-      return deferred.reject().promise();
-    }
-
-    self
-      .getStore()
-      .getItem(connectorType + '_' + contextType)
-      .then(saved => {
-        if (saved && saved.version === CONTEXT_CATALOG_VERSION) {
-          deferred.resolve(saved.entry);
-        } else {
-          deferred.reject();
-        }
-      })
-      .catch(error => {
-        console.warn(error);
-        deferred.reject();
-      });
-
-    return deferred.promise();
-  }
-
-  /**
-   * @param {Object} options
-   * @param {Connector} options.connector
-   * @param {boolean} [options.clearCache] - Default False
-   * @param {boolean} [options.silenceErrors] - Default False
-   * @return {Promise}
-   */
-  getNamespaces(options) {
-    const self = this;
-    const connectorId = options.connector.id;
-
-    const notifyForRefresh = self.namespacePromises[connectorId] && options.clearCache;
-    if (options.clearCache) {
-      self.namespacePromises[connectorId] = undefined;
-      self.namespaces[connectorId] = undefined;
-    }
-
-    if (self.namespacePromises[connectorId]) {
-      return self.namespacePromises[connectorId];
-    }
-
-    if (self.namespaces[connectorId]) {
-      self.namespacePromises[connectorId] = $.Deferred()
-        .resolve(self.namespaces[connectorId])
-        .promise();
-      return self.namespacePromises[connectorId];
-    }
-
-    const deferred = $.Deferred();
-
-    self.namespacePromises[connectorId] = deferred.promise();
-
-    const startingNamespaces = {};
-    const pollTimeout = -1;
-
-    const pollForStarted = () => {
-      window.clearTimeout(pollTimeout);
-      window.setTimeout(() => {
-        if (Object.keys(startingNamespaces).length) {
-          apiHelper.fetchContextNamespaces(options).done(namespaces => {
-            if (namespaces[connectorId]) {
-              const namespaces = namespaces[connectorId];
-              if (namespaces) {
-                let statusChanged = false;
-                namespaces.forEach(namespace => {
-                  if (startingNamespaces[namespace.id] && namespace.status !== 'STARTING') {
-                    startingNamespaces[namespace.id].status = namespace.status;
-                    delete startingNamespaces[namespace.id];
-                    statusChanged = true;
-                  }
-                });
-                if (statusChanged) {
-                  huePubSub.publish(NAMESPACES_REFRESHED_EVENT, connectorId);
-                }
-                if (Object.keys(startingNamespaces).length) {
-                  pollForStarted();
-                }
-              }
-            }
-          });
-        }
-      }, 2000);
-    };
-
-    deferred.done(context => {
-      context.namespaces.forEach(namespace => {
-        if (namespace.status === 'STARTING') {
-          startingNamespaces[namespace.id] = namespace;
-        }
-      });
-      if (Object.keys(startingNamespaces).length) {
-        pollForStarted();
-      }
-    });
-
-    const fetchNamespaces = () => {
-      apiHelper.fetchContextNamespaces(options).done(namespaces => {
-        if (namespaces[connectorId]) {
-          const dynamic = namespaces.dynamicClusters;
-          namespaces = namespaces[connectorId];
-          if (namespaces) {
-            namespaces.forEach(namespace => {
-              namespace.computes.forEach(compute => {
-                if (!compute.id && compute.crn) {
-                  compute.id = compute.crn;
-                }
-                if (!compute.name && compute.clusterName) {
-                  compute.name = compute.clusterName;
-                }
-              });
-            });
-            self.namespaces[connectorId] = {
-              namespaces: namespaces.filter(namespace => namespace.name),
-              dynamic: dynamic,
-              hueTimestamp: Date.now()
-            };
-            deferred.resolve(self.namespaces[connectorId]);
-            if (notifyForRefresh) {
-              huePubSub.publish(NAMESPACES_REFRESHED_EVENT, connectorId);
-            }
-
-            if (self.namespaces[connectorId].namespaces.length) {
-              self.saveLater(NAMESPACES_CONTEXT_TYPE, connectorId, self.namespaces[connectorId]);
-            } else {
-              self.getStore().removeItem(connectorId + '_' + NAMESPACES_CONTEXT_TYPE);
-            }
-          } else {
-            deferred.reject();
-          }
-        } else {
-          deferred.reject();
-        }
-      });
-    };
-
-    if (!options.clearCache) {
-      self
-        .getSaved(NAMESPACES_CONTEXT_TYPE, connectorId)
-        .done(namespaces => {
-          self.namespaces[connectorId] = namespaces;
-          deferred.resolve(self.namespaces[connectorId]);
-        })
-        .fail(fetchNamespaces);
-    } else {
-      fetchNamespaces();
-    }
-
-    return self.namespacePromises[connectorId];
-  }
-
-  /**
-   * @param {Object} options
-   * @param {Connector} options.connector
-   * @param {boolean} [options.silenceErrors] - Default False
-   * @param {boolean} [options.clearCache] - Default False
-   * @return {Promise}
-   */
-  getComputes(options) {
-    const self = this;
-
-    const connectorId = options.connector.id;
-
-    if (options.clearCache) {
-      self.computePromises[connectorId] = undefined;
-      self.computes[connectorId] = undefined;
-    }
-
-    if (self.computePromises[connectorId]) {
-      return self.computePromises[connectorId];
-    }
-
-    if (self.computes[connectorId]) {
-      self.computePromises[connectorId] = $.Deferred()
-        .resolve(self.computes[connectorId])
-        .promise();
-      return self.computePromises[connectorId];
-    }
-
-    const deferred = $.Deferred();
-    self.computePromises[connectorId] = deferred.promise();
-
-    apiHelper.fetchContextComputes(options).done(computes => {
-      if (computes[connectorId]) {
-        computes = computes[connectorId];
-        if (computes) {
-          self.computes[connectorId] = computes;
-          deferred.resolve(self.computes[connectorId]);
-          // TODO: save
-        } else {
-          deferred.reject();
-        }
-      } else {
-        deferred.reject();
-      }
-    });
-
-    return self.computePromises[connectorId];
-  }
-
-  /**
-   * @param {Object} options
-   * @param {Connector} options.connector
-   * @param {boolean} [options.silenceErrors] - Default False
-   * @return {Promise}
-   */
-  getClusters(options) {
-    const self = this;
-
-    const connectorId = options.connector.id;
-
-    if (self.clusterPromises[connectorId]) {
-      return self.clusterPromises[connectorId];
-    }
-
-    if (self.clusters[connectorId]) {
-      self.clusterPromises[connectorId] = $.Deferred()
-        .resolve(self.clusters[connectorId])
-        .promise();
-      return self.clusterPromises[connectorId];
-    }
-
-    const deferred = $.Deferred();
-    self.clusterPromises[connectorId] = deferred.promise();
-
-    apiHelper.fetchContextClusters(options).done(clusters => {
-      if (clusters && clusters[connectorId]) {
-        self.clusters[connectorId] = clusters[connectorId];
-        deferred.resolve(self.clusters[connectorId]);
-      } else {
-        deferred.reject();
-      }
-    });
-
-    return self.clusterPromises[connectorId];
-  }
-}
-
-const contextCatalog = new ContextCatalog();
-
-export default {
-  /**
-   * @param {Object} options
-   * @param {Connector} options.connector
-   * @param {boolean} [options.clearCache] - Default False
-   * @param {boolean} [options.silenceErrors] - Default False
-   * @return {Promise}
-   */
-  getNamespaces: options => contextCatalog.getNamespaces(options),
-
-  /**
-   * @param {Object} options
-   * @param {Connector} options.connector
-   * @param {boolean} [options.silenceErrors] - Default False
-   * @return {Promise}
-   */
-  getComputes: options => contextCatalog.getComputes(options),
-
-  /**
-   * @param {Object} options
-   * @param {Connector} options.connector
-   * @param {boolean} [options.silenceErrors] - Default False
-   * @return {Promise}
-   */
-  getClusters: options => contextCatalog.getClusters(options)
-};

+ 281 - 0
desktop/core/src/desktop/js/catalog/contextCatalog.ts

@@ -0,0 +1,281 @@
+// 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 localforage from 'localforage';
+
+import { fetchClusters, fetchComputes, fetchNamespaces } from './api';
+import { Cluster, Compute, IdentifiableInterpreter, Namespace } from 'config/types';
+import huePubSub from 'utils/huePubSub';
+import { hueWindow } from 'types/types';
+import { noop } from 'utils/hueUtils';
+import {
+  CONTEXT_CATALOG_REFRESHED_TOPIC,
+  NAMESPACES_REFRESHED_TOPIC,
+  NamespacesRefreshedEvent,
+  REFRESH_CONTEXT_CATALOG_TOPIC
+} from './events';
+
+interface GetOptions {
+  connector: IdentifiableInterpreter;
+  clearCache?: boolean;
+  silenceErrors?: boolean;
+}
+
+interface ConnectorNamespaces {
+  dynamic?: boolean;
+  hueTimestamp: number;
+  namespaces: Namespace[];
+}
+
+enum ContextTypes {
+  Namespace = 'namespace',
+  Compute = 'compute',
+  Cluster = 'cluster'
+}
+
+interface ContextMapping {
+  [ContextTypes.Cluster]: Cluster[];
+  [ContextTypes.Compute]: Compute[];
+  [ContextTypes.Namespace]: ConnectorNamespaces;
+}
+
+const STORAGE_POSTFIX = (<hueWindow>window).LOGGED_USERNAME;
+const CONTEXT_CATALOG_VERSION = 4;
+const DISABLE_CACHE = true;
+
+const store = localforage.createInstance({
+  name: `HueContextCatalog_${STORAGE_POSTFIX}`
+});
+
+const namespacePromises = new Map<string, Promise<ConnectorNamespaces>>();
+const computePromises = new Map<string, Promise<Compute[]>>();
+const clusterPromises = new Map<string, Promise<Cluster[]>>();
+
+huePubSub.subscribe(REFRESH_CONTEXT_CATALOG_TOPIC, async () => {
+  const namespacesToRefresh = [...namespacePromises.keys()];
+  namespacePromises.clear();
+  computePromises.clear();
+  clusterPromises.clear();
+  try {
+    await store.clear();
+  } catch {}
+  huePubSub.publish(CONTEXT_CATALOG_REFRESHED_TOPIC);
+  namespacesToRefresh.forEach(connectorId => {
+    huePubSub.publish<NamespacesRefreshedEvent>(NAMESPACES_REFRESHED_TOPIC, connectorId);
+  });
+});
+
+const saveLaterToCache = <T extends keyof ContextMapping>(
+  type: T,
+  connector: IdentifiableInterpreter,
+  entry: ContextMapping[T]
+) => {
+  if (entry) {
+    window.setTimeout(async () => {
+      try {
+        await store.setItem<{ version: number; entry: ContextMapping[T] }>(
+          `${type}_${connector.id}`,
+          {
+            version: CONTEXT_CATALOG_VERSION,
+            entry
+          }
+        );
+      } catch {}
+    }, 1000);
+  }
+};
+
+const deleteFromCache = async (type: keyof ContextMapping, connector: IdentifiableInterpreter) => {
+  await store.removeItem(`${type}_${connector.id}`);
+};
+
+const getCached = async <T extends keyof ContextMapping>(
+  type: T,
+  connector: IdentifiableInterpreter
+): Promise<ContextMapping[T] | undefined> => {
+  if (!DISABLE_CACHE) {
+    try {
+      const storedItem = await store.getItem<
+        { version: number; entry: ContextMapping[T] } | undefined
+      >(`${type}_${connector.id}`);
+
+      if (storedItem && storedItem.version === CONTEXT_CATALOG_VERSION) {
+        return storedItem.entry;
+      }
+    } catch (err) {
+      console.warn(err);
+    }
+  }
+  return undefined;
+};
+
+export const getNamespaces = async ({
+  connector,
+  clearCache,
+  silenceErrors
+}: GetOptions): Promise<ConnectorNamespaces> => {
+  const notifyForRefresh = namespacePromises.has(connector.id) && clearCache;
+  if (clearCache) {
+    namespacePromises.delete(connector.id);
+    await deleteFromCache(ContextTypes.Namespace, connector);
+  }
+
+  if (!namespacePromises.has(connector.id)) {
+    namespacePromises.set(
+      connector.id,
+      new Promise<ConnectorNamespaces>(async (resolve, reject) => {
+        try {
+          const cached = await getCached(ContextTypes.Namespace, connector);
+          if (cached) {
+            resolve(cached);
+            return;
+          }
+        } catch {}
+
+        const fetchedNamespaces = await fetchNamespaces(connector, silenceErrors);
+        const namespaces = fetchedNamespaces[connector.id];
+        if (namespaces) {
+          const dynamic = fetchedNamespaces.dynamicClusters;
+          namespaces.forEach(namespace => {
+            // Adapt computes, TODO: Still needed?
+            namespace.computes.forEach(
+              (compute: Compute & { crn?: string; clusterName?: string }) => {
+                if (!compute.id && compute.crn) {
+                  compute.id = compute.crn;
+                }
+                if (!compute.name && compute.clusterName) {
+                  compute.name = compute.clusterName;
+                }
+              }
+            );
+          });
+
+          const connectorNamespaces: ConnectorNamespaces = {
+            namespaces: namespaces.filter(namespace => namespace.name),
+            dynamic,
+            hueTimestamp: Date.now()
+          };
+
+          resolve(connectorNamespaces);
+
+          if (notifyForRefresh) {
+            huePubSub.publish<NamespacesRefreshedEvent>(NAMESPACES_REFRESHED_TOPIC, connector.id);
+          }
+
+          if (connectorNamespaces.namespaces.length) {
+            saveLaterToCache(ContextTypes.Namespace, connector, connectorNamespaces);
+          } else {
+            deleteFromCache(ContextTypes.Namespace, connector).catch(noop);
+          }
+        } else {
+          reject();
+        }
+      })
+    );
+  }
+
+  return namespacePromises.get(connector.id)!;
+};
+
+export const getComputes = async ({
+  connector,
+  clearCache,
+  silenceErrors
+}: GetOptions): Promise<Compute[]> => {
+  if (clearCache) {
+    computePromises.delete(connector.id);
+    await deleteFromCache(ContextTypes.Compute, connector);
+  }
+
+  if (!computePromises.has(connector.id)) {
+    computePromises.set(
+      connector.id,
+      new Promise<Compute[]>(async (resolve, reject) => {
+        try {
+          const cached = await getCached(ContextTypes.Compute, connector);
+          if (cached) {
+            resolve(cached);
+            return;
+          }
+        } catch {}
+
+        const fetchedComputes = await fetchComputes(connector, silenceErrors);
+        const computes = fetchedComputes[connector.id];
+        if (computes) {
+          resolve(computes);
+
+          if (computes.length) {
+            saveLaterToCache(ContextTypes.Compute, connector, computes);
+          } else {
+            deleteFromCache(ContextTypes.Compute, connector).catch(noop);
+          }
+        } else {
+          reject();
+        }
+      })
+    );
+  }
+
+  return computePromises.get(connector.id)!;
+};
+
+export const getClusters = async ({
+  connector,
+  clearCache,
+  silenceErrors
+}: GetOptions): Promise<Cluster[]> => {
+  if (clearCache) {
+    clusterPromises.delete(connector.id);
+    await deleteFromCache(ContextTypes.Cluster, connector);
+  }
+
+  if (!clusterPromises.has(connector.id)) {
+    clusterPromises.set(
+      connector.id,
+      new Promise<Cluster[]>(async (resolve, reject) => {
+        try {
+          const cached = await getCached(ContextTypes.Cluster, connector);
+          if (cached) {
+            resolve(cached);
+            return;
+          }
+        } catch {}
+
+        const fetchedClusters = await fetchClusters(connector, silenceErrors);
+        const clusters = fetchedClusters[connector.id];
+        if (clusters) {
+          resolve(clusters);
+
+          if (clusters.length) {
+            saveLaterToCache(ContextTypes.Cluster, connector, clusters);
+          } else {
+            deleteFromCache(ContextTypes.Cluster, connector).catch(noop);
+          }
+        } else {
+          reject();
+        }
+      })
+    );
+  }
+
+  return clusterPromises.get(connector.id)!;
+};
+
+export default {
+  getNamespaces,
+  getComputes,
+  getClusters
+};

+ 22 - 0
desktop/core/src/desktop/js/catalog/events.ts

@@ -0,0 +1,22 @@
+// 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.
+
+export const REFRESH_CONTEXT_CATALOG_TOPIC = 'context.catalog.refresh';
+
+export const CONTEXT_CATALOG_REFRESHED_TOPIC = 'context.catalog.refreshed';
+
+export const NAMESPACES_REFRESHED_TOPIC = 'context.catalog.namespaces.refreshed';
+export type NamespacesRefreshedEvent = string;

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

@@ -15,7 +15,7 @@
 // limitations under the License.
 
 import { CancellablePromise } from 'api/cancellablePromise';
-import contextCatalog from 'catalog/contextCatalog';
+import { getNamespaces } from 'catalog/contextCatalog';
 import { OptimizerMeta, TableSourceMeta } from 'catalog/DataCatalogEntry';
 import { TopAggs, TopColumns, TopFilters, TopJoins, TopJoinValue } from 'catalog/MultiTableEntry';
 import ApiStrategy from 'catalog/optimizer/ApiStrategy';
@@ -152,8 +152,7 @@ export default class SqlAnalyzer implements Optimizer {
     const path = options.paths[0].join('.');
 
     return new CancellablePromise<TopJoins>((resolve, reject, onCancel) => {
-      contextCatalog
-        .getNamespaces({ connector: this.connector, silenceErrors: !options.silenceErrors })
+      getNamespaces({ connector: this.connector, ...options })
         .then(async (result: { namespaces: Namespace[] }) => {
           if (!result.namespaces.length || !result.namespaces[0].computes.length) {
             reject('No namespace or compute found');

+ 16 - 0
desktop/core/src/desktop/js/config/events.ts

@@ -1,3 +1,19 @@
+// 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 { HueConfig } from './types';
 
 export const REFRESH_CONFIG_TOPIC = 'cluster.config.refresh.config';

+ 0 - 63
desktop/core/src/desktop/js/config/hueConfig.d.ts

@@ -1,63 +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 {
-  AppType,
-  BrowserInterpreter,
-  DashboardInterpreter,
-  EditorInterpreter,
-  HueConfig,
-  Interpreter
-} from './types';
-
-export declare const REFRESH_CONFIG_EVENT = 'cluster.config.refresh.config';
-export declare const CONFIG_REFRESHED_EVENT = 'cluster.config.set.config';
-export declare const GET_KNOWN_CONFIG_EVENT = 'cluster.config.get.config';
-
-export declare const refreshConfig: () => Promise<HueConfig>;
-export declare const getLastKnownConfig: () => HueConfig | undefined;
-export declare const findDashboardConnector: (
-  connectorTest: (connector: Interpreter) => boolean
-) => DashboardInterpreter | undefined;
-export declare const findBrowserConnector: (
-  connectorTest: (connector: Interpreter) => boolean
-) => BrowserInterpreter | undefined;
-export declare const findEditorConnector: (
-  connectorTest: (connector: Interpreter) => boolean
-) => EditorInterpreter | undefined;
-export declare const filterEditorConnectors: (
-  connectorTest: (connector: Interpreter) => boolean
-) => EditorInterpreter[] | undefined;
-
-/**
- * This takes the initial path from the "browser" config, used in cases where the users can't access '/'
- * for abfs etc.
- */
-export declare const getRootFilePath: (connector: BrowserInterpreter) => string;
-
-declare const _default: {
-  refreshConfig: (hueBaseUrl?: string) => Promise<HueConfig>;
-  getInterpreters: (appType: AppType) => Interpreter[];
-  getLastKnownConfig: () => HueConfig;
-  getRootFilePath: (connector: BrowserInterpreter) => string;
-  findBrowserConnector: (connectorTest: (connector: Interpreter) => boolean) => BrowserInterpreter;
-  findDashboardConnector: (
-    connectorTest: (connector: Interpreter) => boolean
-  ) => DashboardInterpreter;
-  findEditorConnector: (connectorTest: (connector: Interpreter) => boolean) => EditorInterpreter;
-};
-
-export default _default;

+ 1 - 1
desktop/core/src/desktop/js/config/types.ts

@@ -105,7 +105,7 @@ export interface Connector extends IdentifiableInterpreter {
 }
 
 export interface EditorInterpreter extends IdentifiableInterpreter {
-  dialect_properties: Record<string, unknown> | null;
+  dialect_properties?: Record<string, unknown>;
   is_batchable: boolean;
   is_sql: boolean;
   name: string;

+ 1 - 0
desktop/core/src/desktop/js/hue.js

@@ -58,6 +58,7 @@ import sqlUtils from 'sql/sqlUtils';
 import sqlWorkerHandler from 'sql/sqlWorkerHandler';
 
 import 'components/sidebar/HueSidebarWebComponent';
+import 'components/assist/AssistPanelWebComponent';
 
 import 'ko/components/assist/assistViewModel';
 import OnePageViewModel from 'onePageViewModel';

+ 10 - 8
desktop/core/src/desktop/js/jquery/plugins/jquery.hiveautocomplete.js

@@ -16,7 +16,7 @@
 
 import $ from 'jquery';
 
-import contextCatalog from 'catalog/contextCatalog';
+import { getNamespaces } from 'catalog/contextCatalog';
 import dataCatalog from 'catalog/dataCatalog';
 import { hueLocalStorage } from 'utils/storageUtils';
 
@@ -59,13 +59,15 @@ function Plugin(element, options) {
   if (self.options.namespace) {
     self.namespaceDeferred.resolve(self.options.namespace);
   } else {
-    contextCatalog.getNamespaces({ connector: { id: options.apiHelperType } }).done(context => {
-      if (context.namespaces && context.namespaces.length) {
-        self.namespaceDeferred.resolve(context.namespaces[0]);
-      } else {
-        self.namespaceDeferred.reject();
-      }
-    });
+    getNamespaces({ connector: { id: options.apiHelperType } })
+      .then(context => {
+        if (context.namespaces && context.namespaces.length) {
+          self.namespaceDeferred.resolve(context.namespaces[0]);
+        } else {
+          self.namespaceDeferred.reject();
+        }
+      })
+      .catch();
   }
   self.namespaceDeferred.done(namespace => {
     if (

+ 14 - 14
desktop/core/src/desktop/js/ko/components/assist/assistDbSource.js

@@ -17,7 +17,8 @@
 import $ from 'jquery';
 import * as ko from 'knockout';
 
-import contextCatalog, { NAMESPACES_REFRESHED_EVENT } from 'catalog/contextCatalog';
+import { getComputes, getNamespaces } from 'catalog/contextCatalog';
+import { NAMESPACES_REFRESHED_TOPIC } from 'catalog/events';
 import AssistDbNamespace from 'ko/components/assist/assistDbNamespace';
 import huePubSub from 'utils/huePubSub';
 import { getFromLocalStorage } from 'utils/storageUtils';
@@ -27,8 +28,8 @@ class AssistDbSource {
    * @param {Object} options
    * @param {Object} options.i18n
    * @param {string} options.type
-   * @param {ContextNamespace} [options.initialNamespace] - Optional initial namespace to use
-   * @param {ContextCompute} [options.initialCompute] - Optional initial compute to use
+   * @param {Namespace} [options.initialNamespace] - Optional initial namespace to use
+   * @param {Compute} [options.initialCompute] - Optional initial compute to use
    * @param {Connector} options.connector
    * @param {string} options.name
    * @param {boolean} options.nonSqlType - Optional, default false
@@ -107,15 +108,14 @@ class AssistDbSource {
 
     self.hasNamespaces = ko.pureComputed(() => self.namespaces().length > 0);
 
-    huePubSub.subscribe(NAMESPACES_REFRESHED_EVENT, connectorId => {
+    huePubSub.subscribe(NAMESPACES_REFRESHED_TOPIC, connectorId => {
       if (self.connector.id !== connectorId) {
         return;
       }
 
       self.loading(true);
-      contextCatalog
-        .getNamespaces({ connector: self.connector })
-        .done(context => {
+      getNamespaces({ connector: self.connector })
+        .then(context => {
           const newNamespaces = [];
           const existingNamespaceIndex = {};
           self.namespaces().forEach(assistNamespace => {
@@ -142,7 +142,8 @@ class AssistDbSource {
           });
           self.namespaces(newNamespaces);
         })
-        .always(() => {
+        .catch()
+        .finally(() => {
           self.loading(false);
         });
     });
@@ -158,12 +159,11 @@ class AssistDbSource {
     self.loading(true);
 
     if (refresh) {
-      contextCatalog.getComputes({ connector: self.connector, clearCache: true });
+      getComputes({ connector: self.connector, clearCache: true }).catch();
     }
 
-    return contextCatalog
-      .getNamespaces({ connector: self.connector, clearCache: refresh })
-      .done(context => {
+    return getNamespaces({ connector: self.connector, clearCache: refresh })
+      .then(context => {
         const assistNamespaces = [];
         let activeNamespace;
         let activeCompute;
@@ -208,10 +208,10 @@ class AssistDbSource {
           }
         }
       })
-      .fail(() => {
+      .catch(() => {
         self.hasErrors(true);
       })
-      .always(() => {
+      .finally(() => {
         self.loadedDeferred.resolve();
         self.loading(false);
       });

+ 21 - 19
desktop/core/src/desktop/js/ko/components/contextPopover/ko.contextPopover.js

@@ -28,7 +28,7 @@ import LangRefContext from './langRefContext';
 import PartitionContext, { PARTITION_CONTEXT_TEMPLATE } from './partitionContext';
 import ResizeHelper from './resizeHelper';
 import StorageContext from './storageContext';
-import contextCatalog from 'catalog/contextCatalog';
+import { getNamespaces } from 'catalog/contextCatalog';
 import dataCatalog from 'catalog/dataCatalog';
 import { GET_KNOWN_CONFIG_TOPIC } from 'config/events';
 import { findEditorConnector } from 'config/hueConfig';
@@ -932,24 +932,26 @@ class SqlContextContentsGlobalSearch {
         // TODO: Global search results are referring to dialect and not type
         connector = findEditorConnector(connector => connector.dialect === connectorId);
       }
-      contextCatalog.getNamespaces({ connector: connector }).done(context => {
-        dataCatalog
-          .getEntry({
-            namespace: context.namespaces[0],
-            compute: context.namespaces[0].computes[0],
-            connector: connector,
-            path: path,
-            definition: { type: params.data.type.toLowerCase() }
-          })
-          .then(catalogEntry => {
-            catalogEntry.navigatorMeta = params.data;
-            catalogEntry.navigatorMetaPromise = CancellablePromise.resolve(
-              catalogEntry.navigatorMeta
-            );
-            catalogEntry.saveLater();
-            self.contents(new DataCatalogContext({ popover: self, catalogEntry: catalogEntry }));
-          });
-      });
+      getNamespaces({ connector })
+        .then(context => {
+          dataCatalog
+            .getEntry({
+              namespace: context.namespaces[0],
+              compute: context.namespaces[0].computes[0],
+              connector: connector,
+              path: path,
+              definition: { type: params.data.type.toLowerCase() }
+            })
+            .then(catalogEntry => {
+              catalogEntry.navigatorMeta = params.data;
+              catalogEntry.navigatorMetaPromise = CancellablePromise.resolve(
+                catalogEntry.navigatorMeta
+              );
+              catalogEntry.saveLater();
+              self.contents(new DataCatalogContext({ popover: self, catalogEntry: catalogEntry }));
+            });
+        })
+        .catch();
     } else if (self.isDocument) {
       self.contents(new DocumentContext(params.data));
     } else if (self.isPartition) {

+ 7 - 8
desktop/core/src/desktop/js/ko/components/ko.contextSelector.js

@@ -19,11 +19,9 @@ import * as ko from 'knockout';
 
 import { ASSIST_SET_DATABASE_EVENT } from './assist/events';
 import componentUtils from './componentUtils';
-import contextCatalog, {
-  CONTEXT_CATALOG_REFRESHED_EVENT,
-  NAMESPACES_REFRESHED_EVENT
-} from 'catalog/contextCatalog';
+import contextCatalog from 'catalog/contextCatalog';
 import dataCatalog from 'catalog/dataCatalog';
+import { CONTEXT_CATALOG_REFRESHED_TOPIC, NAMESPACES_REFRESHED_TOPIC } from 'catalog/events';
 import huePubSub from 'utils/huePubSub';
 import I18n from 'utils/i18n';
 import { getFromLocalStorage, setInLocalStorage } from 'utils/storageUtils';
@@ -222,8 +220,8 @@ const HueContextSelector = function (params) {
     }
   };
 
-  const namespaceRefreshSub = huePubSub.subscribe(NAMESPACES_REFRESHED_EVENT, refresh);
-  const contextCatalogRefreshSub = huePubSub.subscribe(CONTEXT_CATALOG_REFRESHED_EVENT, refresh);
+  const namespaceRefreshSub = huePubSub.subscribe(NAMESPACES_REFRESHED_TOPIC, refresh);
+  const contextCatalogRefreshSub = huePubSub.subscribe(CONTEXT_CATALOG_REFRESHED_TOPIC, refresh);
   self.disposals.push(() => {
     window.clearTimeout(refreshThrottle);
     namespaceRefreshSub.remove();
@@ -321,7 +319,7 @@ HueContextSelector.prototype.reload = function (type) {
     self[type.lastPromise] = contextCatalog[type.contextCatalogFn]({
       connector: ko.unwrap(self.connector)
     })
-      .done(available => {
+      .then(available => {
         // Namespaces response differs slightly from the others
         if (type === TYPES_INDEX.namespace) {
           available = available.namespaces;
@@ -380,7 +378,8 @@ HueContextSelector.prototype.reload = function (type) {
           self.setMatchingCompute(self[type.name]());
         }
       })
-      .always(() => {
+      .catch()
+      .finally(() => {
         self[type.loading](false);
       });
   } else {

+ 2 - 2
desktop/core/src/desktop/static/desktop/js/jquery.hiveautocomplete.js

@@ -53,13 +53,13 @@
     if (self.options.namespace) {
       self.namespaceDeferred.resolve(self.options.namespace);
     } else {
-      contextCatalog.getNamespaces({ connector: { id: options.apiHelperType } }).done(function (context) {
+      contextCatalog.getNamespaces({ connector: { id: options.apiHelperType } }).then(function (context) {
         if (context.namespaces && context.namespaces.length) {
           self.namespaceDeferred.resolve(context.namespaces[0]);
         } else {
           self.namespaceDeferred.reject();
         }
-      })
+      }).catch();
     }
     self.namespaceDeferred.done(function (namespace) {
       if (!self.options.compute || !namespace.computes.some(function (compute) {

+ 5 - 0
desktop/core/src/desktop/templates/hue.mako

@@ -167,6 +167,10 @@ ${ hueIcons.symbols() }
       <div class="left-panel" data-bind="css: { 'side-panel-closed': !leftAssistVisible() }, visibleOnHover: { selector: '.hide-left-side-panel' }">
         <a href="javascript:void(0);" style="z-index: 1002; display: none;" title="${_('Show Assist')}" class="pointer side-panel-toggle show-left-side-panel" data-bind="visible: !leftAssistVisible(), toggle: leftAssistVisible"><i class="fa fa-chevron-right"></i></a>
         <a href="javascript:void(0);" style="display: none; opacity: 0;" title="${_('Hide Assist')}" class="pointer side-panel-toggle hide-left-side-panel" data-bind="visible: leftAssistVisible, toggle: leftAssistVisible"><i class="fa fa-chevron-left"></i></a>
+        <!-- ko if: window.USE_NEW_ASSIST_PANEL -->
+          <assist-panel-web-component></assist-panel-web-component>
+        <!-- /ko -->
+        <!-- ko ifnot: window.USE_NEW_ASSIST_PANEL -->
         <div class="assist" data-bind="component: {
             name: 'assist-panel',
             params: {
@@ -180,6 +184,7 @@ ${ hueIcons.symbols() }
               visibleAssistPanels: ['sql']
             }
           }, visible: leftAssistVisible" style="display:none;"></div>
+        <!-- /ko -->
       </div>
 
       <div id="leftResizer" class="resizer" data-bind="visible: leftAssistVisible(), splitFlexDraggable : {

+ 2 - 2
desktop/libs/dashboard/src/dashboard/static/dashboard/js/search.ko.js

@@ -559,11 +559,11 @@ var Collection = function (vm, collection) {
   self.activeNamespace = ko.observable();
   self.activeCompute = ko.observable();
 
-  contextCatalog.getNamespaces({ connector: { id: collection.engine || 'solr' } }).done(function (context) {
+  contextCatalog.getNamespaces({ connector: { id: collection.engine || 'solr' } }).then(function (context) {
     // TODO: Namespace selection
     self.activeNamespace(context.namespaces[0]);
     self.activeCompute(context.namespaces[0].computes[0]);
-  });
+  }).catch();
 
   self.simpleAceDatabase = ko.pureComputed(function () {
     return self.name().split('.')[0];

+ 2 - 2
desktop/libs/indexer/src/indexer/templates/importer.mako

@@ -2584,7 +2584,7 @@ ${ commonheader(_("Importer"), "indexer", user, request, "60px") | n,unicode }
       self.computeSetDeferred = $.Deferred();
 
       // TODO: Use connectors in the importer
-      contextCatalog.getNamespaces({ connector: { id: vm.sourceType } }).done(function (context) {
+      contextCatalog.getNamespaces({ connector: { id: vm.sourceType } }).then(function (context) {
         self.namespaces(context.namespaces);
         if (!vm.namespaceId || !context.namespaces.some(function (namespace) {
           if (namespace.id === vm.namespaceId) {
@@ -2619,7 +2619,7 @@ ${ commonheader(_("Importer"), "indexer", user, request, "60px") | n,unicode }
           }
         })
         self.computeSetDeferred.resolve();
-      });
+      }).catch();
 
       self.fileType = ko.observable();
       self.fileType.subscribe(function (newType) {

+ 2 - 2
desktop/libs/indexer/src/indexer/templates/indexes.mako

@@ -651,11 +651,11 @@ ${ commonheader(_("Index Browser"), "search", user, request, "60px") | n,unicode
       self.activeCompute = ko.observable();
 
       // TODO: Use connectors in indexes
-      contextCatalog.getNamespaces({ connector: { id: 'solr' }}).done(function (context) {
+      contextCatalog.getNamespaces({ connector: { id: 'solr' }}).then(function (context) {
         // TODO: Namespace selection
         self.activeNamespace(context.namespaces[0]);
         self.activeCompute(context.namespaces[0].computes[0]);
-      });
+      }).catch();
 
       self.assistAvailable = ko.observable(true);
       self.apiHelper = window.apiHelper;

+ 2 - 2
desktop/libs/indexer/src/indexer/templates/topics.mako

@@ -582,11 +582,11 @@ ${ commonheader(_("Streams Browser"), "search", user, request, "60px") | n,unico
       self.activeCompute = ko.observable();
 
       // TODO: Use connectors in topics
-      contextCatalog.getNamespaces({ connector: { id: 'solr' } }).done(function (context) {
+      contextCatalog.getNamespaces({ connector: { id: 'solr' } }).then(function (context) {
         // TODO: Namespace selection
         self.activeNamespace(context.namespaces[0]);
         self.activeCompute(context.namespaces[0].computes[0]);
-      });
+      }).catch();
 
       self.assistAvailable = ko.observable(true);
       self.apiHelper = window.apiHelper;