Browse Source

HUE-8330 [metastore] Introduce MetastoreSource in the table browser

Johan Ahlen 7 years ago
parent
commit
4b7a2e2fdc

+ 115 - 180
apps/metastore/src/metastore/static/metastore/js/metastore.ko.js

@@ -23,6 +23,7 @@ var MetastoreViewModel = (function () {
    */
    */
   function MetastoreViewModel(options) {
   function MetastoreViewModel(options) {
     var self = this;
     var self = this;
+
     self.partitionsLimit = options.partitionsLimit;
     self.partitionsLimit = options.partitionsLimit;
     self.assistAvailable = ko.observable(true);
     self.assistAvailable = ko.observable(true);
     self.apiHelper = ApiHelper.getInstance();
     self.apiHelper = ApiHelper.getInstance();
@@ -31,131 +32,106 @@ var MetastoreViewModel = (function () {
     self.apiHelper.withTotalStorage('assist', 'assist_panel_visible', self.isLeftPanelVisible, true);
     self.apiHelper.withTotalStorage('assist', 'assist_panel_visible', self.isLeftPanelVisible, true);
     self.optimizerEnabled = ko.observable(options.optimizerEnabled || false);
     self.optimizerEnabled = ko.observable(options.optimizerEnabled || false);
     self.navigatorEnabled = ko.observable(options.navigatorEnabled || false);
     self.navigatorEnabled = ko.observable(options.navigatorEnabled || false);
-    self.sourceType = ko.observable(options.sourceType || 'hive');
 
 
-    self.navigatorEnabled.subscribe(function (newValue) {
-      huePubSub.publish('meta.navigator.enabled', newValue);
-    });
+    self.source = ko.observable();
+    self.sources = ko.observableArray();
 
 
-    self.optimizerUrl = ko.observable(options.optimizerUrl);
-    self.navigatorUrl = ko.observable(options.navigatorUrl);
-
-    huePubSub.subscribe("assist.db.panel.ready", function () {
-      huePubSub.publish('assist.set.database', {
-        source: self.sourceType(),
-        namespace: self.namespace(),
-        name: null
+    self.source.subscribe(function (newValue) {
+      newValue.loadNamespaces().done(function () {
+        if (newValue.namespace()) {
+          newValue.namespace().loadDatabases().done(function () {
+            self.loadUrl();
+          });
+        }
       });
       });
     });
     });
 
 
-    self.reloading = ko.observable(false);
-    self.loading = ko.observable(false);
-
-    self.currentTab = ko.observable('');
-
-    self.lastLoadNamespacesDeferred = $.Deferred();
-    self.namespace = ko.observable();
-    self.namespaces = ko.observableArray();
-
-    self.namespace.subscribe(function () {
-      if (self.namespace() && self.namespace().databases().length === 0) {
-        self.namespace().loadDatabases();
+    self.loading = ko.pureComputed(function () {
+      if (!self.source()) {
+        return true;
       }
       }
-    });
-
-    huePubSub.subscribe('data.catalog.entry.refreshed', function (details) {
-      var refreshedEntry = details.entry;
-
-      if (refreshedEntry.getSourceType() !== self.sourceType()) {
-        return;
+      if (!self.source().namespace()) {
+        return true;
       }
       }
+      return false;
+    });
 
 
-      var prevNamespaceId = null;
-      var prevDbName = null;
-      var prevTableName = null;
-      if (self.namespace()) {
-        prevNamespaceId = self.namespace().namespace.id;
-        if (self.namespace().database()) {
-          prevDbName = self.namespace().database().catalogEntry.name;
-          if (self.namespace().database().table()) {
-            prevTableName = self.namespace().database().table().catalogEntry.name;
+    huePubSub.publish('cluster.config.get.config', function (clusterConfig) {
+      var initialSourceType = options.sourceType || 'hive';
+      if (clusterConfig && clusterConfig.app_config && clusterConfig.app_config.editor && clusterConfig.app_config.editor.interpreters) {
+        var sources = [];
+        clusterConfig.app_config.editor.interpreters.forEach(function (interpreter) {
+          if (interpreter.is_sql) {
+            sources.push(new MetastoreSource({
+              metastoreViewModel: self,
+              name: interpreter.name,
+              type: interpreter.type
+            }))
           }
           }
-
+        });
+        if (!sources.length) {
+          sources.push(new MetastoreSource({
+            metastoreViewModel: self,
+            name: initialSourceType,
+            type: initialSourceType
+          }));
+        }
+        self.sources(sources);
+        var found = sources.some(function (source) {
+          if (source.type === initialSourceType) {
+            self.source(source);
+            return true;
+          }
+        });
+        if (!found) {
+          self.source(sources[0]);
         }
         }
       }
       }
+    });
 
 
-      var setPrevious = function () {
-        if (prevNamespaceId) {
-          self.setNamespaceById(prevNamespaceId).done(function () {
-            if (prevDbName) {
-              self.namespace().setDatabaseByName(prevDbName, function () {
-                if (self.namespace().database() && prevTableName) {
-                  self.namespace().database().setTableByName(prevTableName);
-                }
-              });
-            }
-          });
-        }
-      };
+    self.navigatorEnabled.subscribe(function (newValue) {
+      huePubSub.publish('meta.navigator.enabled', newValue);
+    });
 
 
-      var completeRefresh = function () {
-        self.reloading(true);
-        if (self.namespace() && self.namespace().database() && self.namespace().database().table()) {
-          self.namespace().database().table(null);
-        }
-        if (self.namespace() && self.namespace().database()) {
-          self.namespace().database(null);
-        }
-        if (self.namespace()) {
-          self.namespace(null);
-        }
-        self.loadNamespaces().done(setPrevious).always(function () {
-          self.reloading(false);
-        });
-      };
+    self.optimizerUrl = ko.observable(options.optimizerUrl);
+    self.navigatorUrl = ko.observable(options.navigatorUrl);
+
+    self.currentTab = ko.observable('');
 
 
-      if (refreshedEntry.isSource()) {
-        completeRefresh();
-      } else if (refreshedEntry.isDatabase() && self.namespace()) {
-        self.namespace().databases().some(function (database) {
-          if (database.catalogEntry === refreshedEntry) {
-            database.load(setPrevious, self.optimizerEnabled(), self.navigatorEnabled());
+    huePubSub.subscribe('assist.database.selected', function (databaseDef) {
+      if (self.source().type !== databaseDef.sourceType) {
+        var found = self.sources().some(function (source) {
+          if (source.type === databaseDef.sourceType) {
+            self.source(source);
             return true;
             return true;
           }
           }
-        })
-      } else if (refreshedEntry.isTableOrView()) {
-        self.namespace().databases().some(function (database) {
-          if (database.catalogEntry.name === refreshedEntry.path[0]) {
-            database.tables().some(function (table) {
-              if (table.catalogEntry.name === refreshedEntry.name) {
-                table.load();
-                return true;
-              }
-            });
+        });
+        if (!found) {
+          return;
+        }
+      }
+
+      if (self.source().namespace().id !== databaseDef.namespace.id) {
+        var found = self.source().namespaces().some(function (namespace) {
+          if (namespace.id === databaseDef.namespace.id) {
+            self.source().namespace(namespace);
             return true;
             return true;
           }
           }
-        })
+        });
+        if (!found) {
+          return;
+        }
       }
       }
-    });
-
-    huePubSub.subscribe("assist.table.selected", function (tableDef) {
-      self.loadTableDef(tableDef, function () {
+      if (self.source().namespace().database()) {
+        self.source().namespace().database().table(null);
+      }
+      self.source().namespace().setDatabaseByName(databaseDef.name, function () {
         huePubSub.publish('metastore.url.change')
         huePubSub.publish('metastore.url.change')
       });
       });
     });
     });
 
 
-    huePubSub.subscribe('assist.database.selected', function (databaseDef) {
-      if (self.sourceType() !== databaseDef.sourceType) {
-        self.sourceType(databaseDef.sourceType);
-      }
-      if (self.namespace() !== databaseDef.namespace) {
-        self.namespace(databaseDef.namespace);
-      }
-      // TODO: Handle namespaces + sourceType
-      if (self.namespace() && self.database()) {
-        self.namespace().database().table(null);
-      }
-      self.setDatabaseByName(databaseDef.name, function () {
+    huePubSub.subscribe("assist.table.selected", function (tableDef) {
+      self.loadTableDef(tableDef, function () {
         huePubSub.publish('metastore.url.change')
         huePubSub.publish('metastore.url.change')
       });
       });
     });
     });
@@ -165,103 +141,64 @@ var MetastoreViewModel = (function () {
       if (self.isHue4()){
       if (self.isHue4()){
         prefix = '/hue' + prefix;
         prefix = '/hue' + prefix;
       }
       }
-      if (self.namespace()) {
-        if (self.namespace().database() && self.namespace().database().table()) {
-          hueUtils.changeURL(prefix + 'table/' + self.namespace().database().table().catalogEntry.path.join('/'));
-        }
-        else if (self.namespace().database()) {
-          hueUtils.changeURL(prefix + 'tables/' + self.namespace().database().catalogEntry.name);
-        }
-        else {
+      if (self.source() && self.source().namespace()) {
+        if (self.source().namespace().database() && self.source().namespace().database().table()) {
+          hueUtils.changeURL(prefix + 'table/' + self.source().namespace().database().table().catalogEntry.path.join('/'));
+        } else if (self.source().namespace().database()) {
+          hueUtils.changeURL(prefix + 'tables/' + self.source().namespace().database().catalogEntry.name);
+        } else {
           hueUtils.changeURL(prefix + 'databases');
           hueUtils.changeURL(prefix + 'databases');
         }
         }
       }
       }
     });
     });
 
 
-    self.loadNamespaces().done(function () {
-      self.loadUrl();
-    });
-
     window.onpopstate = function () {
     window.onpopstate = function () {
       if (window.location.pathname.indexOf('/metastore') > -1) {
       if (window.location.pathname.indexOf('/metastore') > -1) {
         self.loadUrl();
         self.loadUrl();
       }
       }
     };
     };
 
 
-    self.namespacesBreadcrumb = function () {
-      if (self.namespace() && self.namespace().database()) {
-        self.namespace().database().table(null);
-      }
-      if (self.namespace()) {
-        self.namespace().database(null)
-      }
-      self.namespace(null);
-      huePubSub.publish('metastore.url.change');
-    };
-
     self.databasesBreadcrumb = function () {
     self.databasesBreadcrumb = function () {
-      if (self.namespace().database()) {
-        self.namespace().database().table(null);
+      if (self.source().namespace().database()) {
+        self.source().namespace().database().table(null);
       }
       }
-      self.namespace().database(null);
+      self.source().namespace().database(null);
       huePubSub.publish('metastore.url.change');
       huePubSub.publish('metastore.url.change');
     };
     };
 
 
     self.tablesBreadcrumb = function () {
     self.tablesBreadcrumb = function () {
-      self.namespace().database().table(null);
+      self.source().namespace().database().table(null);
       huePubSub.publish('metastore.url.change')
       huePubSub.publish('metastore.url.change')
     }
     }
   }
   }
 
 
-  MetastoreViewModel.prototype.loadNamespaces = function () {
-    var self = this;
-    self.lastLoadNamespacesDeferred = $.Deferred();
-    ContextCatalog.getNamespaces({ sourceType: self.sourceType() }).done(function (namespaces) {
-      self.namespaces($.map(namespaces, function (namespace) {
-        return new MetastoreNamespace({
-          metastoreViewModel: self,
-          sourceType: self.sourceType,
-          navigatorEnabled: self.navigatorEnabled,
-          optimizerEnabled: self.optimizerEnabled,
-          namespace: namespace
-        });
-      }));
-      self.namespace(self.namespaces()[0]);
-      self.lastLoadNamespacesDeferred.resolve();
-    }).fail(self.lastLoadNamespacesDeferred.reject);
-    return self.lastLoadNamespacesDeferred;
-  };
-
-  MetastoreViewModel.prototype.setNamespaceById = function (namespaceId) {
+  MetastoreViewModel.prototype.loadTableDef = function (tableDef, callback) {
     var self = this;
     var self = this;
-    var deferred = $.Deferred();
-    self.lastLoadNamespacesDeferred.done(function () {
-      var found = self.namespaces().some(function (namespace) {
-        if (namespace.namespace.id === namespaceId) {
-          self.namespace(namespace);
+    if (self.source().type !== tableDef.sourceType) {
+      var found = self.sources().some(function (source) {
+        if (source.type === tableDef.sourceType) {
+          self.source(source);
           return true;
           return true;
-          deferred.resolve();
         }
         }
       });
       });
       if (!found) {
       if (!found) {
-        deferred.reject();
+        return;
       }
       }
-    }).fail(deferred.reject);
-    return deferred.promise();
-  }
-
-
-  MetastoreViewModel.prototype.loadTableDef = function (tableDef, callback) {
-    var self = this;
-    if (self.sourceType() !== tableDef.sourceType) {
-      self.sourceType(tableDef.sourceType);
     }
     }
-    if (self.namespace() !== tableDef.namespace) {
-      self.namespace(tableDef.namespace);
+    if (self.source().namespace().id !== tableDef.namespace.id) {
+      var found = self.source().namespaces().some(function (namespace) {
+        if (namespace.id === tableDef.namespace.id) {
+          self.source().namespace(namespace);
+          return true;
+        }
+      });
+      if (!found) {
+        return;
+      }
     }
     }
-    self.namespace().setDatabaseByName(tableDef.database, function () {
-      if (self.namespace() && self.namespace().database()) {
-        if (self.namespace().database().table() && self.namespace().database().table().catalogEntry.name === tableDef.name) {
+    self.source().namespace().setDatabaseByName(tableDef.database, function () {
+      if (self.source().namespace().database()) {
+        if (self.source().namespace().database().table() && self.source().namespace().database().table().catalogEntry.name === tableDef.name) {
           if (callback) {
           if (callback) {
             callback();
             callback();
           }
           }
@@ -269,24 +206,22 @@ var MetastoreViewModel = (function () {
         }
         }
 
 
         var setTableAfterLoad = function (clearDbCacheOnMissing) {
         var setTableAfterLoad = function (clearDbCacheOnMissing) {
-          var foundTables = self.namespace().database().tables().filter(function (table) {
+          var foundTables = self.source().namespace().database().tables().filter(function (table) {
             return table.catalogEntry.name === tableDef.name;
             return table.catalogEntry.name === tableDef.name;
           });
           });
           if (foundTables.length === 1) {
           if (foundTables.length === 1) {
-            self.namespace().database().setTable(foundTables[0], callback);
+            self.source().namespace().database().setTable(foundTables[0], callback);
           } else if (clearDbCacheOnMissing) {
           } else if (clearDbCacheOnMissing) {
-            self.namespace().database().catalogEntry.clearCache({ invalidate: 'invalidate', silenceErrors: true }).done(function () {
-              self.namespace().database().load(function () {
+            self.source().namespace().database().catalogEntry.clearCache({ invalidate: 'invalidate', silenceErrors: true }).done(function () {
+              self.source().namespace().database().load(function () {
                 setTableAfterLoad(false);
                 setTableAfterLoad(false);
               });
               });
             });
             });
-          } else {
-            self.loadingTable(false);
           }
           }
         };
         };
 
 
-        if (!self.namespace().database().loaded()) {
-          var doOnce = self.namespace().database().loaded.subscribe(function () {
+        if (!self.source().namespace().database().loaded()) {
+          var doOnce = self.source().namespace().database().loaded.subscribe(function () {
             setTableAfterLoad(true);
             setTableAfterLoad(true);
             doOnce.dispose();
             doOnce.dispose();
           });
           });
@@ -311,7 +246,7 @@ var MetastoreViewModel = (function () {
     if (path[0] === 'metastore') {
     if (path[0] === 'metastore') {
       path.shift();
       path.shift();
     }
     }
-    var namespace = self.namespace();
+    var namespace = self.source().namespace();
     switch (path[0]) {
     switch (path[0]) {
       case 'databases':
       case 'databases':
         if (namespace.database()) {
         if (namespace.database()) {
@@ -332,7 +267,7 @@ var MetastoreViewModel = (function () {
         self.loadTableDef({
         self.loadTableDef({
           name: path[2],
           name: path[2],
           database: path[1],
           database: path[1],
-          sourceType: self.sourceType(),
+          sourceType: self.source().type,
           namespace: namespace
           namespace: namespace
         }, function(){
         }, function(){
           if (path.length > 3 && path[3] === 'partitions'){
           if (path.length > 3 && path[3] === 'partitions'){

+ 159 - 3
apps/metastore/src/metastore/static/metastore/js/metastore.model.js

@@ -14,11 +14,167 @@
 // See the License for the specific language governing permissions and
 // See the License for the specific language governing permissions and
 // limitations under the License.
 // limitations under the License.
 
 
+var MetastoreSource = (function () {
+
+  var MetastoreSource = function (options) {
+    var self = this;
+    self.type = options.type;
+    self.name = options.name;
+    self.metastoreViewModel = options.metastoreViewModel;
+
+    self.reloading = ko.observable(false);
+    self.loading = ko.observable(false);
+
+    self.namespace = ko.observable();
+    self.namespaces = ko.observableArray();
+
+    self.lastLoadNamespacesDeferred = $.Deferred();
+    self.namespace = ko.observable();
+    self.namespaces = ko.observableArray();
+
+    self.namespace.subscribe(function () {
+      if (self.namespace() && self.namespace().databases().length === 0) {
+        self.namespace().loadDatabases();
+      }
+    });
+
+    huePubSub.subscribe("assist.db.panel.ready", function () {
+      self.lastLoadNamespacesDeferred.done(function () {
+        huePubSub.publish('assist.set.database', {
+          source: self.type,
+          namespace: self.namespace().namespace,
+          name: null
+        });
+      });
+    });
+
+    huePubSub.subscribe('data.catalog.entry.refreshed', function (details) {
+      var refreshedEntry = details.entry;
+
+      if (refreshedEntry.getSourceType() !== self.type) {
+        return;
+      }
+
+      var prevNamespaceId = null;
+      var prevDbName = null;
+      var prevTableName = null;
+      if (self.namespace()) {
+        prevNamespaceId = self.namespace().id;
+        if (self.namespace().database()) {
+          prevDbName = self.namespace().database().catalogEntry.name;
+          if (self.namespace().database().table()) {
+            prevTableName = self.namespace().database().table().catalogEntry.name;
+          }
+
+        }
+      }
+
+      var setPrevious = function () {
+        if (prevNamespaceId) {
+          self.setNamespaceById(prevNamespaceId).done(function () {
+            if (prevDbName) {
+              self.namespace().setDatabaseByName(prevDbName, function () {
+                if (self.namespace().database() && prevTableName) {
+                  self.namespace().database().setTableByName(prevTableName);
+                }
+              });
+            }
+          });
+        }
+      };
+
+      var completeRefresh = function () {
+        self.reloading(true);
+        if (self.namespace() && self.namespace().database() && self.namespace().database().table()) {
+          self.namespace().database().table(null);
+        }
+        if (self.namespace() && self.namespace().database()) {
+          self.namespace().database(null);
+        }
+        if (self.namespace()) {
+          self.namespace(null);
+        }
+        self.loadNamespaces().done(setPrevious).always(function () {
+          self.reloading(false);
+        });
+      };
+
+      if (refreshedEntry.isSource()) {
+        completeRefresh();
+      } else if (refreshedEntry.isDatabase() && self.namespace()) {
+        self.namespace().databases().some(function (database) {
+          if (database.catalogEntry === refreshedEntry) {
+            database.load(setPrevious, self.optimizerEnabled(), self.navigatorEnabled());
+            return true;
+          }
+        })
+      } else if (refreshedEntry.isTableOrView()) {
+        self.namespace().databases().some(function (database) {
+          if (database.catalogEntry.name === refreshedEntry.path[0]) {
+            database.tables().some(function (table) {
+              if (table.catalogEntry.name === refreshedEntry.name) {
+                table.load();
+                return true;
+              }
+            });
+            return true;
+          }
+        })
+      }
+    });
+  };
+
+  MetastoreSource.prototype.loadNamespaces = function () {
+    var self = this;
+    self.loading(true);
+    ContextCatalog.getNamespaces({ sourceType: self.type }).done(function (namespaces) {
+      self.namespaces($.map(namespaces, function (namespace) {
+        return new MetastoreNamespace({
+          metastoreViewModel: self.metastoreViewModel,
+          sourceType: self.type,
+          navigatorEnabled: self.metastoreViewModel.navigatorEnabled,
+          optimizerEnabled: self.metastoreViewModel.optimizerEnabled,
+          namespace: namespace
+        });
+      }));
+      self.namespace(self.namespaces()[0]);
+      self.lastLoadNamespacesDeferred.resolve();
+    }).fail(self.lastLoadNamespacesDeferred.reject).always(function () {
+      self.loading(false);
+    });
+    return self.lastLoadNamespacesDeferred;
+  };
+
+
+  MetastoreSource.prototype.setNamespaceById = function (namespaceId) {
+    var self = this;
+    var deferred = $.Deferred();
+    self.lastLoadNamespacesDeferred.done(function () {
+      var found = self.namespaces().some(function (namespace) {
+        if (namespace.namespace.id === namespaceId) {
+          self.namespace(namespace);
+          deferred.resolve();
+          return true;
+        }
+      });
+      if (!found) {
+        deferred.reject();
+      }
+    }).fail(deferred.reject);
+    return deferred.promise();
+  }
+
+  return MetastoreSource;
+})();
+
 var MetastoreNamespace = (function () {
 var MetastoreNamespace = (function () {
 
 
   var MetastoreNamespace = function (options) {
   var MetastoreNamespace = function (options) {
     var self = this;
     var self = this;
+    self.apiHelper = ApiHelper.getInstance();
     self.namespace = options.namespace;
     self.namespace = options.namespace;
+    self.id = options.namespace.id;
+    self.name = options.namespace.name;
     self.metastoreViewModel = options.metastoreViewModel;
     self.metastoreViewModel = options.metastoreViewModel;
     self.sourceType = options.sourceType;
     self.sourceType = options.sourceType;
     self.navigatorEnabled = options.navigatorEnabled;
     self.navigatorEnabled = options.navigatorEnabled;
@@ -49,7 +205,7 @@ var MetastoreNamespace = (function () {
       self.loadingDatabases(false);
       self.loadingDatabases(false);
     });
     });
 
 
-    DataCatalog.getEntry({ namespace: self.namespace, sourceType: self.sourceType(), path: [], definition: { type: 'source' } }).done(function (entry) {
+    DataCatalog.getEntry({ namespace: self.namespace, sourceType: self.sourceType, path: [], definition: { type: 'source' } }).done(function (entry) {
       self.catalogEntry(entry);
       self.catalogEntry(entry);
       entry.getChildren().done(function (databaseEntries) {
       entry.getChildren().done(function (databaseEntries) {
         self.databases($.map(databaseEntries, function (databaseEntry) {
         self.databases($.map(databaseEntries, function (databaseEntry) {
@@ -67,7 +223,7 @@ var MetastoreNamespace = (function () {
     if (!self.reloading() && self.catalogEntry()) {
     if (!self.reloading() && self.catalogEntry()) {
       self.reloading(true);
       self.reloading(true);
       // Clear will publish when done
       // Clear will publish when done
-      self.catalogEntry().clearCache({ invalidate: self.catalogEntry().getSourceType() === 'impala' ? 'invalidate' : 'cache' });
+      self.catalogEntry().clearCache({ invalidate: self.sourceType === 'impala' ? 'invalidate' : 'cache' });
     }
     }
   };
   };
 
 
@@ -77,7 +233,7 @@ var MetastoreNamespace = (function () {
     self.database(metastoreDatabase);
     self.database(metastoreDatabase);
 
 
     if (!metastoreDatabase.loaded()) {
     if (!metastoreDatabase.loaded()) {
-      metastoreDatabase.load(callback, self.optimizerEnabled(), self.navigatorEnabled(), self.sourceType());
+      metastoreDatabase.load(callback, self.optimizerEnabled(), self.navigatorEnabled(), self.sourceType);
     } else if (callback) {
     } else if (callback) {
       callback();
       callback();
     }
     }

+ 29 - 63
apps/metastore/src/metastore/templates/metastore.mako

@@ -82,36 +82,24 @@ ${ components.menubar(is_embeddable) }
 <link rel="stylesheet" href="${ static('metastore/css/metastore.css') }" type="text/css">
 <link rel="stylesheet" href="${ static('metastore/css/metastore.css') }" type="text/css">
 
 
 <script type="text/html" id="metastore-breadcrumbs">
 <script type="text/html" id="metastore-breadcrumbs">
-  <ul class="nav nav-pills hue-breadcrumbs-bar" id="breadcrumbs">
-    <!-- ko if: namespaces().length > 1-->
-    <li>
-      <a href="javascript:void(0);" data-bind="click: namespacesBreadcrumb">${_('Namespaces')}
-        <!-- ko if: namespace -->
-        <span class="divider">&gt;</span>
-        <!-- /ko -->
-      </a>
-    </li>
+  <div style="font-size: 14px; margin: 0 12px; line-height: 27px;">
+    <div data-bind="component: { name: 'hue-drop-down', params: { value: source, entries: sources, labelAttribute: 'name', searchable: true, linkTitle: '${ _ko('Source') }' } }" style="display: inline-block"></div>
+    <!-- ko with: source -->
+    <!-- ko if: namespaces().length > 1 -->
+    <div class="margin-left-10" data-bind="component: { name: 'hue-drop-down', params: { value: namespace, entries: namespaces, labelAttribute: 'name', searchable: true, linkTitle: '${ _ko('Namespace') }' } }" style="display: inline-block"></div>
     <!-- /ko -->
     <!-- /ko -->
-    <!-- ko if: namespaces().length <= 1-->
-    <li>
-      <a href="javascript:void(0);" data-bind="click: databasesBreadcrumb">${_('Databases')}
-        <!-- ko if: namespace() && namespace().database() -->
-        <span class="divider">&gt;</span>
-        <!-- /ko -->
-      </a>
-    </li>
     <!-- /ko -->
     <!-- /ko -->
+  </div>
+  <ul style="padding-top: 0" class="nav nav-pills hue-breadcrumbs-bar" id="breadcrumbs">
+    <!-- ko with: source -->
     <!-- ko with: namespace -->
     <!-- ko with: namespace -->
-    <!-- ko if: $parent.namespaces().length > 1 -->
     <li>
     <li>
-      <a href="javascript:void(0);" data-bind="click: $root.databasesBreadcrumb">
-        <span data-bind="text: namespace.name"></span>
+      <a href="javascript:void(0);" data-bind="click: $root.databasesBreadcrumb">${_('Databases')}
         <!-- ko if: database -->
         <!-- ko if: database -->
         <span class="divider">&gt;</span>
         <span class="divider">&gt;</span>
         <!-- /ko -->
         <!-- /ko -->
       </a>
       </a>
     </li>
     </li>
-    <!-- /ko -->
     <!-- ko with: database -->
     <!-- ko with: database -->
     <li>
     <li>
       <a href="javascript:void(0);" data-bind="click: $root.tablesBreadcrumb">
       <a href="javascript:void(0);" data-bind="click: $root.tablesBreadcrumb">
@@ -129,12 +117,13 @@ ${ components.menubar(is_embeddable) }
     <!-- ko if: editingTable -->
     <!-- ko if: editingTable -->
       <!-- ko with: table -->
       <!-- ko with: table -->
       <li class="editable-breadcrumb-input">
       <li class="editable-breadcrumb-input">
-        <input type="text" data-bind="hivechooser: { data: catalogEntry.name, database: $parent.catalogEntry. name, skipColumns: true, searchEverywhere: true, onChange: function(val) { $parent.setTableByName(val); $parent.editingTable(false); }, apiHelperUser: '${ user }', apiHelperType: $root.sourceType()}" autocomplete="off" />
+        <input type="text" data-bind="hivechooser: { data: catalogEntry.name, database: $parent.catalogEntry. name, skipColumns: true, searchEverywhere: true, onChange: function(val) { $parent.setTableByName(val); $parent.editingTable(false); }, apiHelperUser: '${ user }', apiHelperType: $root.source().type }" autocomplete="off" />
       </li>
       </li>
       <!-- /ko -->
       <!-- /ko -->
     <!-- /ko -->
     <!-- /ko -->
     <!-- /ko -->
     <!-- /ko -->
     <!-- /ko -->
     <!-- /ko -->
+    <!-- /ko -->
   </ul>
   </ul>
 </script>
 </script>
 
 
@@ -176,7 +165,7 @@ ${ components.menubar(is_embeddable) }
           </th>
           </th>
           <th>${_('Values')}</th>
           <th>${_('Values')}</th>
           <th>${_('Spec')}</th>
           <th>${_('Spec')}</th>
-          <th data-bind="visible: $root.sourceType() != 'impala'">${_('Browse')}</th>
+          <th data-bind="visible: $root.source().type !== 'impala'">${_('Browse')}</th>
         </tr>
         </tr>
       </thead>
       </thead>
       <tbody>
       <tbody>
@@ -192,14 +181,14 @@ ${ components.menubar(is_embeddable) }
         </td>
         </td>
         <td title="${_('Query partition data')}">
         <td title="${_('Query partition data')}">
           <!-- ko if: IS_HUE_4 -->
           <!-- ko if: IS_HUE_4 -->
-            <a data-bind="click: function() { queryAndWatch(notebookUrl, $root.sourceType()); }, text: '[\'' + columns.join('\',\'') + '\']'" href="javascript:void(0)"></a>
+            <a data-bind="click: function() { queryAndWatch(notebookUrl, $root.source().type); }, text: '[\'' + columns.join('\',\'') + '\']'" href="javascript:void(0)"></a>
           <!-- /ko -->
           <!-- /ko -->
           <!-- ko if: ! IS_HUE_4 -->
           <!-- ko if: ! IS_HUE_4 -->
             <a data-bind="attr: { 'href': readUrl }, text: '[\'' + columns.join('\',\'') + '\']'"></a>
             <a data-bind="attr: { 'href': readUrl }, text: '[\'' + columns.join('\',\'') + '\']'"></a>
           <!-- /ko -->
           <!-- /ko -->
         </td>
         </td>
         <td data-bind="text: partitionSpec"></td>
         <td data-bind="text: partitionSpec"></td>
-        <td data-bind="visible: $root.sourceType() != 'impala'">
+        <td data-bind="visible: $root.source().type != 'impala'">
           <!-- ko if: IS_HUE_4 -->
           <!-- ko if: IS_HUE_4 -->
             <a data-bind="click: function () { browsePartitionFolder(browseUrl); }" href="javascript:void(0)" title="${_('Browse partition files')}">
             <a data-bind="click: function () { browsePartitionFolder(browseUrl); }" href="javascript:void(0)" title="${_('Browse partition files')}">
               ${_('Files')}
               ${_('Files')}
@@ -353,33 +342,6 @@ ${ components.menubar(is_embeddable) }
   <!-- /ko -->
   <!-- /ko -->
 </script>
 </script>
 
 
-<script type="text/html" id="metastore-namespaces">
-  <div class="entries-table-container">
-    <div class="actionbar-actions">
-
-    </div>
-    <div class="entries-table-no-header">
-      <div class="catalog-entries-list-container">
-        <!-- ko hueSpinner: { spin: loading, center: true, size: 'xlarge' } --><!-- /ko -->
-        <!-- ko if: !loading() -->
-        <table id="entryTable" class="table table-condensed table-nowrap">
-          <thead>
-            <tr>
-              <th>${ _("Namespace") }</th>
-            </tr>
-          </thead>
-          <tbody data-bind="foreach: namespaces">
-            <tr>
-              <td><a href="javascript: void(0);" data-bind="text: namespace.name, click: function () { $parent.namespace($data) }"></a></td>
-            </tr>
-          </tbody>
-        </table>
-        <!-- /ko -->
-      </div>
-    </div>
-  </div>
-</script>
-
 <script type="text/html" id="metastore-databases">
 <script type="text/html" id="metastore-databases">
   <div class="entries-table-container">
   <div class="entries-table-container">
     <div class="actionbar-actions">
     <div class="actionbar-actions">
@@ -391,7 +353,7 @@ ${ components.menubar(is_embeddable) }
           <form action="/metastore/databases/drop" data-bind="submit: dropAndWatch" method="POST">
           <form action="/metastore/databases/drop" data-bind="submit: dropAndWatch" method="POST">
             <input type="hidden" name="is_embeddable" value="true"/>
             <input type="hidden" name="is_embeddable" value="true"/>
             <input type="hidden" name="start_time" value=""/>
             <input type="hidden" name="start_time" value=""/>
-            <input type="hidden" name="source_type" data-bind="value: $root.sourceType"/>
+            <input type="hidden" name="source_type" data-bind="value: $root.source().type"/>
         % else:
         % else:
           <form id="dropDatabaseForm" action="/metastore/databases/drop" method="POST">
           <form id="dropDatabaseForm" action="/metastore/databases/drop" method="POST">
         % endif
         % endif
@@ -552,7 +514,7 @@ ${ components.menubar(is_embeddable) }
       <h4 class="entries-table-header">${ _('Tables') }</h4>
       <h4 class="entries-table-header">${ _('Tables') }</h4>
       <div class="actionbar-actions" data-bind="visible: tables().length > 0">
       <div class="actionbar-actions" data-bind="visible: tables().length > 0">
         <button class="btn toolbarBtn margin-left-20" title="${_('Browse the selected table')}" data-bind="click: function () { onTableClick(selectedTables()[0].catalogEntry); selectedTables([]); }, disable: selectedTables().length !== 1"><i class="fa fa-eye"></i> ${_('View')}</button>
         <button class="btn toolbarBtn margin-left-20" title="${_('Browse the selected table')}" data-bind="click: function () { onTableClick(selectedTables()[0].catalogEntry); selectedTables([]); }, disable: selectedTables().length !== 1"><i class="fa fa-eye"></i> ${_('View')}</button>
-        <button class="btn toolbarBtn" title="${_('Query the selected table')}" data-bind="click: function () { IS_HUE_4 ? queryAndWatch('/notebook/browse/' + selectedTables()[0].catalogEntry.path.join('/') + '/', $root.sourceType()) : location.href = '/notebook/browse/' + selectedTables()[0].catalogEntry.path.join('/'); }, disable: selectedTables().length !== 1">
+        <button class="btn toolbarBtn" title="${_('Query the selected table')}" data-bind="click: function () { IS_HUE_4 ? queryAndWatch('/notebook/browse/' + selectedTables()[0].catalogEntry.path.join('/') + '/', $root.source().type) : location.href = '/notebook/browse/' + selectedTables()[0].catalogEntry.path.join('/'); }, disable: selectedTables().length !== 1">
           <i class="fa fa-play fa-fw"></i> ${_('Query')}
           <i class="fa fa-play fa-fw"></i> ${_('Query')}
         </button>
         </button>
         % if has_write_access:
         % if has_write_access:
@@ -578,7 +540,7 @@ ${ components.menubar(is_embeddable) }
       <form data-bind="attr: { 'action': '/metastore/tables/drop/' + catalogEntry.name }, submit: dropAndWatch" method="POST">
       <form data-bind="attr: { 'action': '/metastore/tables/drop/' + catalogEntry.name }, submit: dropAndWatch" method="POST">
         <input type="hidden" name="is_embeddable" value="true"/>
         <input type="hidden" name="is_embeddable" value="true"/>
         <input type="hidden" name="start_time" value=""/>
         <input type="hidden" name="start_time" value=""/>
-        <input type="hidden" name="source_type" data-bind="value: $root.sourceType"/>
+        <input type="hidden" name="source_type" data-bind="value: $root.source().type"/>
     % else:
     % else:
       <form data-bind="attr: { 'action': '/metastore/tables/drop/' +catalogEntry. name }" method="POST">
       <form data-bind="attr: { 'action': '/metastore/tables/drop/' +catalogEntry. name }" method="POST">
     % endif
     % endif
@@ -694,7 +656,7 @@ ${ components.menubar(is_embeddable) }
         <input type="hidden" name="is_embeddable" value="true"/>
         <input type="hidden" name="is_embeddable" value="true"/>
         <input type="hidden" name="format" value="json"/>
         <input type="hidden" name="format" value="json"/>
         <input type="hidden" name="start_time" value=""/>
         <input type="hidden" name="start_time" value=""/>
-        <input type="hidden" name="source_type" data-bind="value: $root.sourceType"/>
+        <input type="hidden" name="source_type" data-bind="value: $root.source().type"/>
     % else:
     % else:
       <form data-bind="attr: { 'action': '/metastore/table/' + $parent.catalogEntry.path.join('/')+ '/partitions/drop' }" method="POST">
       <form data-bind="attr: { 'action': '/metastore/table/' + $parent.catalogEntry.path.join('/')+ '/partitions/drop' }" method="POST">
     % endif
     % endif
@@ -1032,12 +994,13 @@ ${ components.menubar(is_embeddable) }
           <!-- ko ifnot: loading -->
           <!-- ko ifnot: loading -->
           <h1>
           <h1>
             <div class="inline-block pull-right">
             <div class="inline-block pull-right">
+              <!-- ko with: source -->
               <!-- ko with: namespace -->
               <!-- ko with: namespace -->
               <!-- ko with: database -->
               <!-- ko with: database -->
               <!-- ko with: table -->
               <!-- ko with: table -->
               % if USE_NEW_EDITOR.get():
               % if USE_NEW_EDITOR.get():
                 <!-- ko if: IS_HUE_4 -->
                 <!-- ko if: IS_HUE_4 -->
-                <a href="javascript: void(0);" class="btn btn-default" data-bind="click: function() { queryAndWatch('/notebook/browse/' + catalogEntry.path.join('/') + '/', $root.sourceType()); }" title="${_('Query')}"><i class="fa fa-play fa-fw"></i> ${_('Query')}</a>
+                <a href="javascript: void(0);" class="btn btn-default" data-bind="click: function() { queryAndWatch('/notebook/browse/' + catalogEntry.path.join('/') + '/', $root.source().type); }" title="${_('Query')}"><i class="fa fa-play fa-fw"></i> ${_('Query')}</a>
                 <!-- /ko -->
                 <!-- /ko -->
                 <!-- ko if: ! IS_HUE_4 -->
                 <!-- ko if: ! IS_HUE_4 -->
                 <a class="btn btn-default" data-bind="attr: { 'href': '/notebook/browse/' + catalogEntry.path.join('/') }" title="${_('Query')}"><i class="fa fa-play fa-fw"></i> ${_('Query')}</a>
                 <a class="btn btn-default" data-bind="attr: { 'href': '/notebook/browse/' + catalogEntry.path.join('/') }" title="${_('Query')}"><i class="fa fa-play fa-fw"></i> ${_('Query')}</a>
@@ -1061,12 +1024,13 @@ ${ components.menubar(is_embeddable) }
               <a href="javascript: void(0);" class="btn btn-default" data-bind="click: reload" title="${_('Refresh')}"><i class="fa fa-refresh" data-bind="css: { 'fa-spin blue' : loading }"></i> ${_('Refresh')}</a>
               <a href="javascript: void(0);" class="btn btn-default" data-bind="click: reload" title="${_('Refresh')}"><i class="fa fa-refresh" data-bind="css: { 'fa-spin blue' : loading }"></i> ${_('Refresh')}</a>
               <!-- /ko -->
               <!-- /ko -->
               <!-- /ko -->
               <!-- /ko -->
+              <!-- /ko -->
             </div>
             </div>
 
 
             <!-- ko template: 'metastore-breadcrumbs' --><!-- /ko -->
             <!-- ko template: 'metastore-breadcrumbs' --><!-- /ko -->
           </h1>
           </h1>
 
 
-          <!-- ko template: { if: !loading() && namespace() === null, name: 'metastore-namespaces' } --><!-- /ko -->
+          <!-- ko with: source -->
           <!-- ko with: namespace -->
           <!-- ko with: namespace -->
           <!-- ko template: { if: !loading() && database() === null, name: 'metastore-databases' } --><!-- /ko -->
           <!-- ko template: { if: !loading() && database() === null, name: 'metastore-databases' } --><!-- /ko -->
           <!-- ko with: database -->
           <!-- ko with: database -->
@@ -1078,18 +1042,19 @@ ${ components.menubar(is_embeddable) }
           <!-- /ko -->
           <!-- /ko -->
           <!-- /ko -->
           <!-- /ko -->
           <!-- /ko -->
           <!-- /ko -->
+          <!-- /ko -->
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
   </div>
   </div>
-
-  <!-- ko with: namespace() -->
+  <!-- ko with: source -->
+  <!-- ko with: namespace -->
   <div id="dropSingleTable" class="modal hide fade">
   <div id="dropSingleTable" class="modal hide fade">
     % if is_embeddable:
     % if is_embeddable:
     <form data-bind="submit: dropAndWatch" method="POST">
     <form data-bind="submit: dropAndWatch" method="POST">
       <input type="hidden" name="is_embeddable" value="true"/>
       <input type="hidden" name="is_embeddable" value="true"/>
       <input type="hidden" name="start_time" value=""/>
       <input type="hidden" name="start_time" value=""/>
-      <input type="hidden" name="source_type" data-bind="value: $root.sourceType"/>
+      <input type="hidden" name="source_type" data-bind="value: $root.source().type"/>
     % else:
     % else:
     <form method="POST">
     <form method="POST">
     % endif
     % endif
@@ -1109,6 +1074,7 @@ ${ components.menubar(is_embeddable) }
     </form>
     </form>
   </div>
   </div>
   <!-- /ko -->
   <!-- /ko -->
+  <!-- /ko -->
   <div id="import-data-modal" class="modal hide fade" style="display: block;width: 680px;margin-left: -340px!important;"></div>
   <div id="import-data-modal" class="modal hide fade" style="display: block;width: 680px;margin-left: -340px!important;"></div>
 </div>
 </div>
 </span>
 </span>
@@ -1287,8 +1253,8 @@ ${ components.menubar(is_embeddable) }
       ko.applyBindings(viewModel, $('#metastoreComponents')[0]);
       ko.applyBindings(viewModel, $('#metastoreComponents')[0]);
 
 
       if (location.getParameter('refresh') === 'true') {
       if (location.getParameter('refresh') === 'true') {
-        DataCatalog.getEntry({ namespace: viewModel.activeNamespace(), sourceType: viewModel.sourceType(), path: [], definition: { type: 'source' }}).done(function (entry) {
-          entry.clearCache({ invalidate: sourceType() === 'impala' ? 'invalidate' : 'cache', silenceErrors: true });
+        DataCatalog.getEntry({ namespace: viewModel.source().namespace().namespace, sourceType: viewModel.source().type, path: [], definition: { type: 'source' }}).done(function (entry) {
+          entry.clearCache({ invalidate: viewMode.source().type === 'impala' ? 'invalidate' : 'cache', silenceErrors: true });
           hueUtils.replaceURL('?');
           hueUtils.replaceURL('?');
         });
         });
       }
       }