Browse Source

HUE-7820 [metastore] Use the DataCatalog in the table browser

Johan Ahlen 7 years ago
parent
commit
8510c85

+ 43 - 50
apps/metastore/src/metastore/static/metastore/js/metastore.ko.js

@@ -63,12 +63,12 @@ var MetastoreViewModel = (function () {
 
     self.currentTab = ko.observable('');
 
-    self.filteredDatabases = ko.computed(function () {
+    self.filteredDatabases = ko.pureComputed(function () {
       if (self.databaseQuery() === '') {
         return self.databases();
       }
-      return $.grep(self.databases(), function (database) {
-        return database.name.toLowerCase().indexOf(self.databaseQuery()) > -1;
+      return self.databases().filter(function (database) {
+        return database.catalogEntry.name.toLowerCase().indexOf(self.databaseQuery().toLowerCase()) !== -1;
       });
     });
 
@@ -99,24 +99,23 @@ var MetastoreViewModel = (function () {
       var currentDatabase = null;
       var currentTable = null;
       if (self.database()) {
-        currentDatabase = self.database().name;
+        currentDatabase = self.database().catalogEntry.name;
         if (self.database().table()) {
-          currentTable = self.database().table().name;
+          currentTable = self.database().table().catalogEntry.name;
           self.database().table(null);
         }
         self.database(null);
       }
-      self.loadDatabases(function () {
+      self.loadDatabases.done(function () {
         if (currentDatabase) {
           self.setDatabaseByName(currentDatabase, function () {
             if (self.database() && currentTable) {
               self.database().setTableByName(currentTable);
             }
-            self.reloading(false);
           });
-        } else {
-          self.reloading(false);
         }
+      }).always(function () {
+        self.reloading(false);
       });
     });
 
@@ -141,10 +140,10 @@ var MetastoreViewModel = (function () {
         prefix = '/hue' + prefix;
       }
       if (self.database() && self.database().table()) {
-        hueUtils.changeURL(prefix + 'table/' + self.database().name + '/' + self.database().table().name);
+        hueUtils.changeURL(prefix + 'table/' + self.database().table().catalogEntry.path.join('/'));
       }
       else if (self.database()) {
-        hueUtils.changeURL(prefix + 'tables/' + self.database().name);
+        hueUtils.changeURL(prefix + 'tables/' + self.database().catalogEntry.name);
       }
       else {
         hueUtils.changeURL(prefix + 'databases');
@@ -173,40 +172,34 @@ var MetastoreViewModel = (function () {
     }
   }
 
-  var lastLoadDatabasesDeferred = null;
+  var lastLoadDatabasesPromise = null;
 
-  MetastoreViewModel.prototype.loadDatabases = function (successCallback) {
+  MetastoreViewModel.prototype.loadDatabases = function () {
     var self = this;
-    if (self.loadingDatabases()) {
-      if (lastLoadDatabasesDeferred !== null) {
-        lastLoadDatabasesDeferred.done(successCallback);
-      }
-      return;
+    if (self.loadingDatabases() && lastLoadDatabasesPromise) {
+      return lastLoadDatabasesPromise;
     }
 
-    lastLoadDatabasesDeferred = $.Deferred();
-    lastLoadDatabasesDeferred.done(successCallback);
-
     self.loadingDatabases(true);
-    self.apiHelper.loadDatabases({
-      sourceType: self.sourceType(),
-      successCallback: function (databaseNames) {
-        self.databases($.map(databaseNames, function (name) {
-          return new MetastoreDatabase({
-            name: name,
-            optimizerEnabled: self.optimizerEnabled,
-            navigatorEnabled: self.navigatorEnabled,
-            sourceType: self.sourceType
-          })
-        }));
-        self.loadingDatabases(false);
-        lastLoadDatabasesDeferred.resolve();
-      },
-      errorCallback: function () {
-        self.databases([]);
-        lastLoadDatabasesDeferred.reject();
-      }
+    var deferred = $.Deferred();
+    lastLoadDatabasesPromise = deferred.promise();
+
+    deferred.fail(function () {
+      self.databases([]);
+    }).always(function () {
+      self.loadingDatabases(false);
     });
+
+    DataCatalog.getEntry({ sourceType: self.sourceType(), path: [] }).done(function (sourceEntry) {
+      sourceEntry.getChildren().done(function (databaseEntries) {
+        self.databases($.map(databaseEntries, function (databaseEntry) {
+          return new MetastoreDatabase({ catalogEntry: databaseEntry, optimizerEnabled: self.optimizerEnabled });
+        }));
+        deferred.resolve();
+      }).fail(deferred.reject);
+    }).fail(deferred.reject);
+
+    return lastLoadDatabasesPromise;
   };
 
   MetastoreViewModel.prototype.loadTableDef = function (tableDef, callback) {
@@ -214,7 +207,7 @@ var MetastoreViewModel = (function () {
     self.loadingTable(true);
     self.setDatabaseByName(tableDef.database, function () {
       if (self.database()) {
-        if (self.database().table() && self.database().table().name == tableDef.name) {
+        if (self.database().table() && self.database().table().catalogEntry.name === tableDef.name) {
           self.loadingTable(false);
           if (callback) {
             callback();
@@ -223,8 +216,8 @@ var MetastoreViewModel = (function () {
         }
 
         var setTableAfterLoad = function (clearDbCacheOnMissing) {
-          var foundTables = $.grep(self.database().tables(), function (table) {
-            return table.name === tableDef.name;
+          var foundTables = self.database().tables().filter(function (table) {
+            return table.catalogEntry.name === tableDef.name;
           });
           if (foundTables.length === 1) {
             self.loadingTable(false);
@@ -233,7 +226,7 @@ var MetastoreViewModel = (function () {
             huePubSub.publish('assist.clear.db.cache', {
               sourceType: self.sourceType(),
               clearAll: false,
-              databaseName: self.database().name
+              databaseName: self.database().catalogEntry.name
             });
             self.database().load(function () {
               setTableAfterLoad(false);
@@ -263,20 +256,20 @@ var MetastoreViewModel = (function () {
         databaseName = self.apiHelper.getFromTotalStorage('editor', 'last.selected.database') ||
             self.apiHelper.getFromTotalStorage('metastore', 'last.selected.database') || 'default';
       }
-      if (self.database() && self.database().name == databaseName) {
+      if (self.database() && self.database().catalogEntry.name === databaseName) {
         if (callback) {
           callback();
         }
         return;
       }
-      var foundDatabases = $.grep(self.databases(), function (database) {
-        return database.name === databaseName;
+      var foundDatabases = self.databases().filter(function (database) {
+        return database.catalogEntry.name === databaseName;
       });
       if (foundDatabases.length === 1) {
         self.setDatabase(foundDatabases[0], callback);
       } else {
-        foundDatabases = $.grep(self.databases(), function (database) {
-          return database.name === 'default';
+        foundDatabases = self.databases().filter(function (database) {
+          return database.catalogEntry.name === 'default';
         });
 
         if (foundDatabases.length === 1) {
@@ -286,8 +279,8 @@ var MetastoreViewModel = (function () {
       }
     };
 
-    if (self.loadingDatabases() && lastLoadDatabasesDeferred !== null) {
-      lastLoadDatabasesDeferred.done(whenLoaded);
+    if (self.loadingDatabases() && lastLoadDatabasesPromise !== null) {
+      lastLoadDatabasesPromise.done(whenLoaded);
     } else {
       whenLoaded();
     }

+ 186 - 311
apps/metastore/src/metastore/static/metastore/js/metastore.model.js

@@ -16,34 +16,34 @@
 
 var MetastoreDatabase = (function () {
   /**
-   * @param {Object} options
-   * @param {string} options.name
-   * @param {string} [options.tableName]
-   * @param {string} [options.tableComment]
+   * @param {object} options
+   * @param {DataCatalogEntry} options.catalogEntry
+   * @param {observable} options.optimizerEnabled
    * @constructor
    */
   function MetastoreDatabase(options) {
     var self = this;
     self.apiHelper = ApiHelper.getInstance();
-    self.name = options.name;
+    self.catalogEntry = options.catalogEntry;
 
     self.loaded = ko.observable(false);
     self.loading = ko.observable(false);
     self.tables = ko.observableArray();
-    self.stats = ko.observable();
-    self.optimizerStats = ko.observableArray(); // TODO to plugify, duplicates similar MetastoreTable
-    self.navigatorStats = ko.observable();
+
+    self.comment = ko.observable();
+
+    self.stats = ko.observable(); // TODO: add to DataCatalogEntry
+    self.navigatorMeta = ko.observable();
 
     self.showAddTagName = ko.observable(false);
     self.addTagName = ko.observable('');
-
     self.tableQuery = ko.observable('').extend({rateLimit: 150});
 
     self.filteredTables = ko.computed(function () {
       var returned = self.tables();
       if (self.tableQuery() !== '') {
-        returned = $.grep(self.tables(), function (table) {
-          return table.name.toLowerCase().indexOf(self.tableQuery()) > -1
+        returned = self.tables().filter(function (table) {
+          return table.catalogEntry.name.toLowerCase().indexOf(self.tableQuery()) > -1
             || (table.comment() && table.comment().toLowerCase().indexOf(self.tableQuery()) > -1);
         });
       }
@@ -63,7 +63,7 @@ var MetastoreDatabase = (function () {
           }
         }
 
-        return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
+        return a.catalogEntry.name.toLowerCase().localeCompare(b.catalogEntry.name.toLowerCase());
       });
     });
 
@@ -71,105 +71,57 @@ var MetastoreDatabase = (function () {
 
     self.editingTable = ko.observable(false);
     self.table = ko.observable(null);
-
-    self.addTags = function () {
-      $.post('/metadata/api/navigator/add_tags', {
-        id: ko.mapping.toJSON(self.navigatorStats().identity),
-        tags: ko.mapping.toJSON([self.addTagName()])
-      }, function(data) {
-        if (data && data.status == 0) {
-          self.navigatorStats().tags.push(self.addTagName());
-          self.addTagName('');
-          self.showAddTagName(false);
-        } else {
-          $(document).trigger("error", data.message);
-        }
-      });
-    };
-
-    self.deleteTags = function (tag) {
-      $.post('/metadata/api/navigator/delete_tags', {
-        id: ko.mapping.toJSON(self.navigatorStats().identity),
-        tags: ko.mapping.toJSON([tag])
-      }, function(data) {
-        if (data && data.status == 0) {
-          self.navigatorStats().tags.remove(tag);
-        } else {
-          $(document).trigger("error", data.message);
-        }
-      });
-    };
   }
 
-  MetastoreDatabase.prototype.load = function (callback, optimizerEnabled, navigatorEnabled, sourceType) {
+  MetastoreDatabase.prototype.load = function (callback, optimizerEnabled, navigatorEnabled) {
     var self = this;
     if (self.loading()) {
       return;
     }
 
     self.loading(true);
-    self.apiHelper.fetchTables({
-      sourceType: sourceType,
-      databaseName: self.name,
-      successCallback: function (data) {
-        self.tables($.map(data.tables_meta, function (tableMeta) {
-          return new MetastoreTable({
-            database: self,
-            name: tableMeta.name,
-            type: tableMeta.type,
-            comment: tableMeta.comment,
-            optimizerEnabled: optimizerEnabled,
-            navigatorEnabled: navigatorEnabled,
-            sourceType: sourceType
+
+    if (navigatorEnabled) {
+      self.catalogEntry.getNavigatorMeta().done(self.navigatorMeta);
+    }
+
+    self.catalogEntry.getComment().done(self.comment);
+
+    self.catalogEntry.getChildren().done(function (tableEntries) {
+      self.tables($.map(tableEntries, function (tableEntry) {
+        return new MetastoreTable({
+          database: self,
+          catalogEntry: tableEntry,
+          optimizerEnabled: optimizerEnabled,
+          navigatorEnabled: navigatorEnabled
+        });
+      }));
+      if (navigatorEnabled) {
+        self.catalogEntry.loadNavigatorMetaForChildren().done(function () {
+          self.tables().forEach(function (table) {
+            table.navigatorMeta(table.catalogEntry.navigatorMeta);
           })
-        }));
-        self.loaded(true);
-        self.loading(false);
-        if (optimizerEnabled && navigatorEnabled) {
-          $.get('/metadata/api/navigator/find_entity', {
-            type: 'database',
-            name: self.name
-          }, function(data){
-            if (data && data.status == 0) {
-              self.navigatorStats(ko.mapping.fromJS(data.entity));
-            } else {
-              //$(document).trigger("info", data.message);
-            }
-          });
-
-          $.post('/metadata/api/optimizer/top_tables', {
-            database: self.name
-          }, function(data){
-            if (data && data.status == 0) {
-              var tableIndex = {};
-              data.top_tables.forEach(function (topTable) {
-                tableIndex[topTable.name] = topTable;
-              });
-              self.tables().forEach(function (table) {
-                table.optimizerStats(tableIndex[table.name]);
-              });
-              self.optimizerStats(data.top_tables);
-            } else {
-              $(document).trigger("error", data.message);
-            }
-          }).always(function () {
-            if (callback) {
-              callback();
-            }
-          });
-        } else if (callback) {
-          callback();
-        }
-      },
-      errorCallback: function (response) {
-        self.loading(false);
-        if (callback) {
-          callback();
-        }
+        })
+      }
+      if (optimizerEnabled) {
+        self.catalogEntry.loadNavOptMetaForChildren().done(function () {
+          self.tables().forEach(function (table) {
+            table.optimizerStats(table.catalogEntry.navOptMeta);
+          })
+        });
+      }
+      self.loaded(true);
+    }).fail(function () {
+      self.tables([]);
+    }).always(function () {
+      self.loading(false);
+      if (callback) {
+        callback();
       }
     });
 
-    $.getJSON('/metastore/databases/' + self.name + '/metadata', function (data) {
+    // TODO: Move to ApiHelper (via DataCatalogEntry)
+    $.getJSON('/metastore/databases/' + self.catalogEntry.name + '/metadata', function (data) {
       if (data && data.status == 0) {
         self.stats(data.data);
       }
@@ -182,7 +134,7 @@ var MetastoreDatabase = (function () {
   MetastoreDatabase.prototype.setTableByName = function (tableName) {
     var self = this;
     var foundTables = $.grep(self.tables(), function (metastoreTable) {
-      return metastoreTable.name === tableName;
+      return metastoreTable.catalogEntry.name === tableName;
     });
 
     if (foundTables.length === 1) {
@@ -245,50 +197,50 @@ var MetastoreTable = (function () {
     self.filters = ko.observableArray([]);
 
     self.typeaheadValues = function (column) {
-      var _vals = [];
+      var values = [];
       self.values().forEach(function (row) {
-        var _cell = row.columns[self.keys().indexOf(column())];
-        if (_vals.indexOf(_cell) == -1) {
-          _vals.push(_cell);
+        var cell = row.columns[self.keys().indexOf(column())];
+        if (values.indexOf(cell) !== -1) {
+          values.push(cell);
         }
       });
-      return _vals
-    }
+      return values
+    };
 
     self.addFilter = function () {
       self.filters.push(ko.mapping.fromJS({'column': '', 'value': ''}));
-    }
+    };
 
     self.removeFilter = function (data) {
       self.filters.remove(data);
-      if (self.filters().length == 0) {
+      if (self.filters().length === 0) {
         self.sortDesc(true);
         self.filter();
       }
-    }
+    };
 
     self.filter = function () {
       self.loading(true);
       self.loaded(false);
-      var _filters = JSON.parse(ko.toJSON(self.filters));
-      var _postData = {};
-      _filters.forEach(function (filter) {
-        _postData[filter.column] = filter.value;
+      var filters = JSON.parse(ko.toJSON(self.filters));
+      var postData = {};
+      filters.forEach(function (filter) {
+        postData[filter.column] = filter.value;
       });
-      _postData["sort"] = self.sortDesc() ? "desc" : "asc";
+      postData['sort'] = self.sortDesc() ? 'desc' : 'asc';
 
       $.ajax({
-        type: "POST",
-        url: '/metastore/table/' + self.metastoreTable.database.name + '/' + self.metastoreTable.name + '/partitions',
-        data: _postData,
+        type: 'POST',
+        url: '/metastore/table/' + self.metastoreTable.catalogEntry.path.join('/') + '/partitions',
+        data: postData,
         success: function (data) {
           self.values(data.partition_values_json);
           self.loading(false);
           self.loaded(true);
         },
-        dataType: "json"
+        dataType: 'json'
       });
-    }
+    };
 
     self.preview = {
       keys: ko.observableArray(),
@@ -301,9 +253,10 @@ var MetastoreTable = (function () {
     if (self.loaded()) {
       return;
     }
+    // TODO: Add to DataCatalogEntry
     self.apiHelper.fetchPartitions({
-      databaseName: self.metastoreTable.database.name,
-      tableName: self.metastoreTable.name,
+      databaseName: self.metastoreTable.catalogEntry.path[0],
+      tableName: self.metastoreTable.catalogEntry.name,
       successCallback: function (data) {
         self.keys(data.partition_keys_json);
         self.values(data.partition_values_json);
@@ -313,7 +266,7 @@ var MetastoreTable = (function () {
         self.loaded(true);
         huePubSub.publish('metastore.loaded.partitions');
       },
-      errorCallback: function (data) {
+      errorCallback: function () {
         self.loading(false);
         self.loaded(true);
       }
@@ -329,7 +282,6 @@ var MetastoreTable = (function () {
     self.rows = ko.observableArray();
     self.headers = ko.observableArray();
     self.metastoreTable = options.metastoreTable;
-    self.apiHelper = ApiHelper.getInstance();
 
     self.hasErrors = ko.observable(false);
     self.errorMessage = ko.observable();
@@ -348,52 +300,44 @@ var MetastoreTable = (function () {
       return;
     }
     self.hasErrors(false);
-    self.apiHelper.fetchTableSample({
-      sourceType: self.metastoreTable.sourceType,
-      databaseName: self.metastoreTable.database.name,
-      tableName: self.metastoreTable.name,
-      silenceErrors: true,
-      successCallback: function (data) {
-        self.rows(data.rows);
-        self.headers(data.headers);
-        self.preview.rows(self.rows().slice(0, 3));
-        self.preview.headers(self.headers());
-        self.loading(false);
-        self.loaded(true);
-      },
-      errorCallback: function (message) {
-        self.errorMessage(message);
-        self.hasErrors(true);
-        self.loading(false);
-        self.loaded(true);
-      }
+    self.loading(true);
+    self.metastoreTable.catalogEntry.getSample().done(function (sample) {
+      self.rows(sample.rows);
+      self.headers(sample.headers);
+      self.preview.rows(self.rows().slice(0, 3));
+      self.preview.headers(self.headers());
+    }).fail(function (message) {
+      self.errorMessage(message);
+      self.hasErrors(true);
+    }).always(function () {
+      self.loading(false);
+      self.loaded(true);
     });
   };
 
   /**
    * @param {Object} options
    * @param {MetastoreDatabase} options.database
-   * @param {string} options.name
-   * @param {string} options.type
-   * @param {string} options.comment
-   * @param {string} options.sourceType
+   * @param {DataCatalogEntry} options.catalogEntry
+   * @param {boolean} options.optimizerEnabled
+   * @param {boolean} options.navigatorEnabled
    * @constructor
    */
   function MetastoreTable(options) {
     var self = this;
     self.database = options.database;
-    self.apiHelper = ApiHelper.getInstance();
     self.optimizerEnabled = options.optimizerEnabled;
     self.navigatorEnabled = options.navigatorEnabled;
-    self.sourceType = options.sourceType;
-    self.name = options.name;
-    self.type = options.type;
-    self.isView = ko.observable(false);
+    self.catalogEntry = options.catalogEntry;
+
+    self.apiHelper = ApiHelper.getInstance();
+
+    // TODO: Check if enough or if we need to fetch additional details
+    self.isView = ko.observable(self.catalogEntry.isView());
 
     self.optimizerStats = ko.observable();
     self.optimizerDetails = ko.observable();
-
-    self.navigatorStats = ko.observable();
+    self.navigatorMeta = ko.observable();
     self.relationshipsDetails = ko.observable();
 
     self.loaded = ko.observable(false);
@@ -401,15 +345,17 @@ var MetastoreTable = (function () {
 
     self.loadingDetails = ko.observable(false);
     self.loadingColumns = ko.observable(false);
-    self.columnQuery = ko.observable('').extend({rateLimit: 150});
+
+    self.columnQuery = ko.observable('').extend({ rateLimit: 150 });
     self.columns = ko.observableArray();
     self.filteredColumns = ko.computed(function () {
       var returned = self.columns();
       if (self.columnQuery() !== '') {
-        returned = $.grep(self.columns(), function (column) {
-          return column.name().toLowerCase().indexOf(self.columnQuery()) > -1
-            || (column.type() && column.type().toLowerCase().indexOf(self.columnQuery()) > -1)
-            || (column.comment() && column.comment().toLowerCase().indexOf(self.columnQuery()) > -1);
+        returned = self.columns().filter(function (column) {
+          var entry = column.catalogEntry;
+          return entry.name.toLowerCase().indexOf(self.columnQuery().toLowerCase()) !== -1
+            || (entry.getType().toLowerCase().indexOf(self.columnQuery().toLowerCase()) !== -1)
+            || (column.comment() && column.comment().toLowerCase().indexOf(self.columnQuery().toLowerCase()) !== -1);
         });
       }
       return returned;
@@ -436,53 +382,33 @@ var MetastoreTable = (function () {
     self.addTagName = ko.observable('');
     self.loadingQueries = ko.observable(true);
 
-    //TODO: Fetch table comment async and don't set it from python
-    self.comment = ko.observable(hueUtils.deXSS(options.comment));
+    self.comment = ko.observable();
+
     self.commentWithoutNewLines = ko.pureComputed(function(){
       return self.comment() ? hueUtils.deXSS(self.comment().replace(/<br\s*[\/]?>/gi, ' ')) : '';
     });
 
     self.comment.subscribe(function (newValue) {
-      var updateCall;
-      var comment = newValue ? newValue : "";
-
-      if (self.navigatorEnabled) {
-        updateCall = $.post('/metadata/api/navigator/update_properties', {
-          id: ko.mapping.toJSON(self.navigatorStats().identity),
-          properties: ko.mapping.toJSON({description: comment})
-        });
-      } else {
-        updateCall = $.post('/metastore/table/' + self.database.name + '/' + self.name + '/alter', {
-          source_type: self.sourceType,
-          comment: comment
-        });
-      }
-
-      updateCall.done(function(data) {
-        if (data && data.status == 0) {
-         huePubSub.publish('assist.clear.db.cache', {
-           sourceType: self.sourceType,
-           databaseName: self.database.name
-         })
-       } else {
-         var message = data.message || data.data;
-         if (message) {
-           $(document).trigger("error", message);
-         }
-       }
-      })
+      self.catalogEntry.getComment().done(function (comment) {
+        if (comment !== newValue) {
+          self.catalogEntry.setComment(newValue).done(self.comment).fail(function () {
+            self.comment(comment);
+          })
+        }
+      });
     });
 
+    // TODO: Move stats to DataCatalogEntry
     self.refreshTableStats = function () {
       if (self.refreshingTableStats()) {
         return;
       }
       self.refreshingTableStats(true);
       self.apiHelper.refreshTableStats({
-        tableName: self.name,
-        databaseName: self.database.name,
-        sourceType: self.sourceType,
-        successCallback: function (data) {
+        tableName: self.catalogEntry.name,
+        databaseName: self.database.catalogEntry.name,
+        sourceType: self.catalogEntry.dataCatalog.sourceType,
+        successCallback: function () {
           self.fetchDetails();
         },
         errorCallback: function (data) {
@@ -495,46 +421,31 @@ var MetastoreTable = (function () {
     };
 
     self.fetchFields = function () {
-      var self = this;
       self.loadingColumns(true);
-      self.apiHelper.fetchFields({
-        sourceType: self.sourceType,
-        databaseName: self.database.name,
-        tableName: self.name,
-        fields: [],
-        successCallback: function (data) {
-          self.loadingColumns(false);
-          self.isView(data.is_view);
-          self.columns($.map(data.extended_columns, function (column) {
-            return new MetastoreColumn({
-              extendedColumn: column,
-              table: self
-            })
-          }));
-/* Get a batch of 500 columns with comments
-          self.apiHelper.navSearch({
-            query: 'parentPath:"/' + self.database.name + '/' + self.name + '" AND type:FIELD AND description:[* TO *]',
-            limit: 500
-          }).done(function (apps) {
-		    console.log(apps);
-		    // For each col update comment
-		  });
-*/
-          self.favouriteColumns(self.columns().slice(0, 5));
-        },
-        errorCallback: function () {
-          self.loadingColumns(false);
-        }
-      })
+      self.catalogEntry.getChildren().done(function (columnEntries) {
+        self.columns($.map(columnEntries, function (columnEntry) {
+          return new MetastoreColumn({
+            catalogEntry: columnEntry,
+            table: self
+          })
+        }));
+        self.favouriteColumns(self.columns().slice(0, 5));
+      }).fail(function () {
+        self.columns([]);
+        self.favouriteColumns([]);
+      }).always(function () {
+        self.loadingColumns(false);
+      });
     };
 
     self.fetchDetails = function () {
-      var self = this;
       self.loadingDetails(true);
+
+      // TODO: Move to DataCatalogEntry
       self.apiHelper.fetchTableDetails({
-        sourceType: self.sourceType,
-        databaseName: self.database.name,
-        tableName: self.name,
+        sourceType: self.catalogEntry.dataCatalog.sourceType,
+        databaseName: self.database.catalogEntry.name,
+        tableName: self.catalogEntry.name,
         successCallback: function (data) {
           self.loadingDetails(false);
           if ((typeof data === 'object') && (data !== null)) {
@@ -551,26 +462,31 @@ var MetastoreTable = (function () {
               self.partitions.loading(false);
               self.partitions.loaded(true);
             }
-            if (self.navigatorEnabled) {
-              $.get('/metadata/api/navigator/find_entity', {
-                type: 'table',
-                database: self.database.name,
-                name: self.name
-              }, function(data) {
-                if (data && data.status == 0) {
-                  self.navigatorStats(ko.mapping.fromJS(data.entity));
-                  //self.getRelationships(); // Off for now
-                } else {
-                  //$(document).trigger("info", data.message);
-                }
-              }).fail(function (xhr, textStatus, errorThrown) {
-                $(document).trigger("error", xhr.responseText);
-              });
-            }
+
+            self.catalogEntry.getComment().done(self.comment);
+            // TODO: Verify that Nav stuff is loaded from parent
+            // if (self.navigatorEnabled) {
+            //   $.get('/metadata/api/navigator/find_entity', {
+            //     type: 'table',
+            //     database: self.database.name,
+            //     name: self.name
+            //   }, function(data) {
+            //     if (data && data.status == 0) {
+            //       self.navigatorMeta(ko.mapping.fromJS(data.entity));
+            //       //self.getRelationships(); // Off for now
+            //     } else {
+            //       //$(document).trigger("info", data.message);
+            //     }
+            //   }).fail(function (xhr, textStatus, errorThrown) {
+            //     $(document).trigger("error", xhr.responseText);
+            //   });
+            // }
+
+            // TODO: Move to DataCatalogEntry
             if (self.optimizerEnabled) {
               $.post('/metadata/api/optimizer/table_details', {
-                databaseName: self.database.name,
-                tableName: self.name
+                databaseName: self.database.catalogEntry.name,
+                tableName: self.catalogEntry.name
               }, function(data){
                 self.loadingQueries(false);
                 if (data && data.status == 0) {
@@ -579,16 +495,15 @@ var MetastoreTable = (function () {
                   // Bump the most important columns first
                   var topCols = $.map(self.optimizerDetails().topCols().slice(0, 5), function(item) { return item.name(); });
                   if (topCols.length >= 3 && self.favouriteColumns().length > 0) {
-                    self.favouriteColumns($.grep(self.columns(), function(col) {
-                        return topCols.indexOf(col.name()) != -1;
-                      })
-                    );
+                    self.favouriteColumns(self.columns().filter(function(col) {
+                      return topCols.indexOf(col.catalogEntry.name) !== -1;
+                    }));
                   }
 
                   // Column popularity, stats
                   $.each(self.optimizerDetails().topCols(), function(index, optimizerCol) {
                     var metastoreCol = $.grep(self.columns(), function(col) {
-                      return col.name() == optimizerCol.name();
+                      return col.catalogEntry.name == optimizerCol.name();
                     });
                     if (metastoreCol.length > 0) {
                       metastoreCol[0].popularity(optimizerCol.score())
@@ -599,8 +514,7 @@ var MetastoreTable = (function () {
                 }
               });
             }
-          }
-          else {
+          } else {
             self.refreshingTableStats(false);
             self.loading(false);
           }
@@ -614,7 +528,7 @@ var MetastoreTable = (function () {
     };
 
     self.drop = function () {
-      $.post('/tables/drop/' + self.database.name, {
+      $.post('/tables/drop/' + self.database.catalogEntry.name, {
         table_selection: ko.mapping.toJSON([self.name]),
         skip_trash: 'off',
         is_embeddable: true
@@ -627,44 +541,16 @@ var MetastoreTable = (function () {
       });
     };
 
-    self.addTags = function () {
-      $.post('/metadata/api/navigator/add_tags', {
-        id: ko.mapping.toJSON(self.navigatorStats().identity),
-        tags: ko.mapping.toJSON([self.addTagName()])
-      }, function(data) {
-        if (data && data.status == 0) {
-          self.navigatorStats().tags.push(self.addTagName());
-          self.addTagName('');
-          self.showAddTagName(false);
-        } else {
-          $(document).trigger("error", data.message);
-        }
-      });
-    };
-
-    self.deleteTags = function (tag) {
-      $.post('/metadata/api/navigator/delete_tags', {
-        id: ko.mapping.toJSON(self.navigatorStats().identity),
-        tags: ko.mapping.toJSON([tag])
-      }, function(data) {
-        if (data && data.status == 0) {
-          self.navigatorStats().tags.remove(tag);
-        } else {
-          $(document).trigger("error", data.message);
-        }
-      });
-    };
-
     self.getRelationships = function () {
       $.post('/metadata/api/navigator/lineage', {
-        id: self.navigatorStats().identity
+        id: self.navigatorMeta().identity
       }, function(data) {
         if (data && data.status == 0) {
           self.relationshipsDetails(ko.mapping.fromJS(data));
         } else {
           $(document).trigger("error", data.message);
         }
-      }).fail(function (xhr, textStatus, errorThrown) {
+      }).fail(function (xhr) {
         $(document).trigger("info", xhr.responseText);
       });
     };
@@ -673,9 +559,9 @@ var MetastoreTable = (function () {
   MetastoreTable.prototype.showImportData = function () {
     var self = this;
     $("#import-data-modal").empty().html('<div class="modal-header"><button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span></button><h2 class="modal-title"></h2></div><div class="modal-body"><i class="fa fa-spinner fa-spin fa-2x muted"></i></div>').modal("show");
-    $.get('/metastore/table/' + self.database.name + '/' + self.name + '/load', function (data) {
+    $.get('/metastore/table/' + self.catalogEntry.path.join('/') + '/load', function (data) {
       $("#import-data-modal").html(data['data']);
-    }).fail(function (xhr, textStatus, errorThrown) {
+    }).fail(function (xhr) {
       $(document).trigger("error", xhr.responseText);
     });
   };
@@ -701,11 +587,11 @@ var MetastoreTable = (function () {
     huePubSub.publish('context.popover.show', {
       data: {
         type: 'table',
-        identifierChain: [{ name: entry.name }]
+        identifierChain: [{ name: entry.catalogEntry.name }]
       },
       orientation: orientation || 'right',
-      sourceType: entry.sourceType,
-      defaultDatabase: entry.database.name,
+      sourceType: entry.catalogEntry.dataCatalog.sourceType,
+      defaultDatabase: entry.database.catalogEntry.name,
       source: {
         element: event.target,
         left: offset.left,
@@ -737,39 +623,28 @@ var MetastoreColumn = (function () {
   /**
    * @param {Object} options
    * @param {MetastoreTable} options.table
-   * @param {object} options.extendedColumn
+   * @param {DataCatalogEntry} options.catalogEntry
    * @constructor
    */
   function MetastoreColumn(options) {
     var self = this;
     self.table = options.table;
-    if (options.extendedColumn && options.extendedColumn.comment) {
-      options.extendedColumn.comment = hueUtils.deXSS(options.extendedColumn.comment);
-    }
-    ko.mapping.fromJS(options.extendedColumn, {}, self);
+    self.catalogEntry = options.catalogEntry;
 
     self.favourite = ko.observable(false);
     self.popularity = ko.observable();
 
+    self.comment = ko.observable();
+
     self.comment.subscribe(function (newValue) {
-      $.post('/metastore/table/' + self.table.database.name + '/' + self.table.name + '/alter_column', {
-        source_type: self.table.sourceType,
-        column: self.name(),
-        comment: newValue
-      }, function (data) {
-        if (data.status == 0) {
-          huePubSub.publish('assist.clear.db.cache', {
-            sourceType: self.table.sourceType,
-            databaseName: self.table.database.name,
-            tableName: self.table.name
-          });
-        } else {
-          $(document).trigger("error", data.message);
+      self.catalogEntry.getComment().done(function (comment) {
+        if (comment !== newValue) {
+          self.catalogEntry.setComment(newValue).done(self.comment).fail(function () {
+            self.comment(comment);
+          })
         }
-      }).fail(function (xhr, textStatus, errorThrown) {
-        $(document).trigger("error", xhr.responseText);
       });
-    })
+    });
   }
 
   MetastoreColumn.prototype.showContextPopover = function (entry, event) {
@@ -778,11 +653,11 @@ var MetastoreColumn = (function () {
     huePubSub.publish('context.popover.show', {
       data: {
         type: 'column',
-        identifierChain: [{ name: entry.table.name }, { name: entry.name() }]
+        identifierChain: [{ name: entry.table.catalogEntry.name }, { name: entry.catalogEntry.name }]
       },
       orientation: 'right',
-      sourceType: entry.table.sourceType,
-      defaultDatabase: entry.table.database.name,
+      sourceType: entry.catalogEntry.dataCatalog.sourceType,
+      defaultDatabase: entry.table.database.catalogEntry.name,
       source: {
         element: event.target,
         left: offset.left,

+ 72 - 73
apps/metastore/src/metastore/templates/metastore.mako

@@ -93,7 +93,7 @@ ${ components.menubar(is_embeddable) }
     <!-- ko with: database -->
     <li>
       <a href="javascript:void(0);" data-bind="click: $root.tablesBreadcrumb">
-        <span data-bind="text: name"></span>
+        <span data-bind="text: catalogEntry.name"></span>
         <!-- ko with: table -->
         <span class="divider">&gt;</span>
         <!-- /ko -->
@@ -101,13 +101,13 @@ ${ components.menubar(is_embeddable) }
     </li>
     <!-- ko with: table -->
     <li class="editable-breadcrumbs" title="${_('Edit path')}" data-bind="click: function(){ $parent.editingTable(true); }, visible: !$parent.editingTable()">
-      <a href="javascript:void(0)" data-bind="text: name"></a>
+      <a href="javascript:void(0)" data-bind="text: catalogEntry.name"></a>
     </li>
     <!-- /ko -->
     <!-- ko if: editingTable -->
       <!-- ko with: table -->
       <li class="editable-breadcrumb-input">
-        <input type="text" data-bind="hivechooser: {data: name, database: $parent.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.sourceType()}" autocomplete="off" />
       </li>
       <!-- /ko -->
     <!-- /ko -->
@@ -127,22 +127,22 @@ ${ components.menubar(is_embeddable) }
         <!-- ko if: $root.optimizerEnabled  -->
           <th width="15%">${_('Popularity')}</th>
         <!-- /ko -->
-        <th width="50%">${_('Comment')}</th>
+        <th width="50%">${_('Description')}</th>
       </tr>
       </thead>
-      <tbody data-bind="hueach: {data: $data, itemHeight: 32, scrollable: '${ MAIN_SCROLLABLE }', scrollableOffset: 200, disableHueEachRowCount: 5, scrollUp: true}">
+      <tbody data-bind="hueach: { data: $data, itemHeight: 32, scrollable: '${ MAIN_SCROLLABLE }', scrollableOffset: 200, disableHueEachRowCount: 5, scrollUp: true}">
         <tr>
           <td data-bind="text: $index() + $indexOffset() + 1"></td>
           <td><a class="blue" href="javascript:void(0)" data-bind="click: showContextPopover"><i class="fa fa-fw fa-info" title="${_('Show details')}"></i></a></td>
           <td title="${ _("Scroll to the column") }">
             <!-- ko if: $root.database() && $root.database().table() && $root.database().table().samples && $root.database().table().samples.loading() -->
-            <span data-bind="text: name"></span> <i class="fa fa-key" data-bind="visible: typeof primary_key !== 'undefined' && primary_key() == 'true'"></i>
+            <span data-bind="text: catalogEntry.name"></span> <i class="fa fa-key" data-bind="visible: catalogEntry.isPrimaryKey()"></i>
             <!-- /ko -->
             <!-- ko ifnot: $root.database() && $root.database().table() && $root.database().table().samples && $root.database().table().samples.loading() -->
-            <a href="javascript:void(0)" class="column-selector" data-bind="click: $root.scrollToColumn"><span data-bind="text: name"></span> <i class="fa fa-key" data-bind="visible: typeof primary_key !== 'undefined' && primary_key() == 'true'"></i></a>
+            <a href="javascript:void(0)" class="column-selector" data-bind="click: $root.scrollToColumn"><span data-bind="text: catalogEntry.name"></span> <i class="fa fa-key" data-bind="visible: catalogEntry.isPrimaryKey()"></i></a>
             <!-- /ko -->
           </td>
-          <td data-bind="text: type"></td>
+          <td data-bind="text: catalogEntry.getType()"></td>
           <!-- ko if: $root.optimizerEnabled  -->
           <td>
             <div class="progress" style="height: 10px; width: 70px; margin-top:5px;" data-bind="attr: { 'title': popularity() }">
@@ -154,9 +154,9 @@ ${ components.menubar(is_embeddable) }
             % if has_write_access:
               <!-- ko ifnot: table.isView() -->
               <div class="show-inactive-on-hover">
-              <a class="inactive-action pointer toggle-editable" title="${ _('Edit the comment') }"><i class="fa fa-pencil"></i></a>
-              <span data-bind="editable: comment, editableOptions: { escape: true, enabled: true, type: 'wysihtml5', toggle: 'manual', skipNewLines: true, toggleElement: '.toggle-editable', placement: 'left', placeholder: '${ _ko('Add a comment...') }', emptytext: '${ _ko('Add a comment...') }', inputclass: 'input-xlarge'}">
-                ${ _('Add a comment...') }</span>
+              <a class="inactive-action pointer toggle-editable" title="${ _('Edit the description') }"><i class="fa fa-pencil"></i></a>
+              <span data-bind="editable: comment, editableOptions: { escape: true, enabled: true, type: 'wysihtml5', toggle: 'manual', skipNewLines: true, toggleElement: '.toggle-editable', placement: 'left', placeholder: '${ _ko('Add a description...') }', emptytext: '${ _ko('Add a description...') }', inputclass: 'input-xlarge'}">
+                ${ _('Add a description...') }</span>
               </div>
               <!-- /ko -->
               <!-- ko if: table.isView() -->
@@ -229,7 +229,7 @@ ${ components.menubar(is_embeddable) }
             <a data-bind="click: function() { queryAndWatch(notebookUrl, $root.sourceType()); }, text: '[\'' + columns.join('\',\'') + '\']'" href="javascript:void(0)"></a>
           <!-- /ko -->
           <!-- 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 -->
         </td>
         <td data-bind="text: partitionSpec"></td>
@@ -240,7 +240,7 @@ ${ components.menubar(is_embeddable) }
             </a>
           <!-- /ko -->
           <!-- ko if: ! IS_HUE_4 -->
-            <a data-bind="attr: {'href': browseUrl }" title="${_('Browse partition files')}">${_('Files')}</a>
+            <a data-bind="attr: { 'href': browseUrl }" title="${_('Browse partition files')}">${_('Files')}</a>
           <!-- /ko -->
         </td>
       </tr>
@@ -390,7 +390,7 @@ ${ components.menubar(is_embeddable) }
           <div class="modal-body">
             <ul data-bind="foreach: selectedDatabases">
               <li>
-                <span data-bind="text: name"></span>
+                <span data-bind="text: catalogEntry.name"></span>
                 <!-- ko if: $data.tables().length > 0 -->
                     (<span data-bind="text: $data.tables().length"></span> tables)
                 <!-- /ko -->
@@ -403,7 +403,7 @@ ${ components.menubar(is_embeddable) }
             <input type="submit" class="btn btn-danger" value="${_('Yes')}"/>
           </div>
           <!-- ko foreach: selectedDatabases -->
-          <input type="hidden" name="database_selection" data-bind="value: name" />
+          <input type="hidden" name="database_selection" data-bind="value: catalogEntry.name" />
           <!-- /ko -->
         </form>
       </div>
@@ -416,13 +416,13 @@ ${ components.menubar(is_embeddable) }
       <th>${ _('Database Name') }</th>
     </tr>
     </thead>
-    <tbody data-bind="hueach: {data: filteredDatabases, itemHeight: 32, scrollable: '${ MAIN_SCROLLABLE }', scrollableOffset: 145, scrollUp: true}">
+    <tbody data-bind="hueach: { data: filteredDatabases, itemHeight: 32, scrollable: '${ MAIN_SCROLLABLE }', scrollableOffset: 145, scrollUp: true }">
     <tr>
       <td width="1%" style="text-align: center">
         <div class="hueCheckbox fa" data-bind="multiCheck: '#databasesTable', value: $data, hueChecked: $parent.selectedDatabases"></div>
       </td>
       <td>
-        <a href="javascript: void(0);" data-bind="text: name, click: function () { $parent.setDatabase($data, function(){ huePubSub.publish('metastore.url.change'); }) }"></a>
+        <a href="javascript: void(0);" data-bind="text: catalogEntry.name, click: function () { $parent.setDatabase($data, function() { huePubSub.publish('metastore.url.change'); }) }"></a>
       </td>
     </tr>
     </tbody>
@@ -436,12 +436,12 @@ ${ components.menubar(is_embeddable) }
       <div class="span12 tile">
           <div class="span6 tile">
             <h4>${ _('Properties') }</h4>
-            <div title="${ _('Comment') }"><i class="fa fa-fw fa-comment muted"></i>
-              <!-- ko if: comment -->
+            <div title="${ _('Description') }"><i class="fa fa-fw fa-comment muted"></i>
+              <!-- ko if: $parent.comment -->
               <span data-bind="text: comment"></span>
               <!-- /ko -->
-              <!-- ko ifnot: comment -->
-              <i>${_('No comment.')}</i>
+              <!-- ko ifnot: $parent.comment -->
+              <i>${_('No description.')}</i>
               <!-- /ko -->
               <div title="${ _('Owner') }">
                 <i class="fa fa-fw fa-user muted"></i>
@@ -449,9 +449,9 @@ ${ components.menubar(is_embeddable) }
                 <br/>
                 <i class="fa fa-fw fa-hdd-o muted"></i>
                 % if IS_EMBEDDED.get():
-                  <span data-bind="attr: {'title': location }"> ${_('Location')}</span>
+                  <span data-bind="attr: { 'title': location }"> ${_('Location')}</span>
                 % else:
-                  <a data-bind="attr: {'href': hdfs_link, 'rel': location }"> ${_('Location')}</a>
+                  <a data-bind="attr: { 'href': hdfs_link, 'rel': location }"> ${_('Location')}</a>
                 % endif
               </div>
             </div>
@@ -462,7 +462,7 @@ ${ components.menubar(is_embeddable) }
             <div style="margin-top: 5px" data-bind="component: { name: 'nav-tags', params: {
               sourceType: $root.sourceType(),
               database: db_name
-              } }"></div>
+            }}"></div>
             <!-- /ko -->
         </div>
         <!-- ko with: parameters -->
@@ -484,7 +484,7 @@ ${ components.menubar(is_embeddable) }
         <div class="actionbar-actions" data-bind="visible: tables().length > 0, dockable: { scrollable: '${ MAIN_SCROLLABLE }', nicescroll: true, jumpCorrection: 5, topSnap: '${ TOP_SNAP }' }">
           <input class="input-xlarge search-query margin-left-10" type="text" placeholder="${ _('Search for a table...') }" data-bind="clearable: tableQuery, value: tableQuery, valueUpdate: 'afterkeydown'"/>
           <button class="btn toolbarBtn margin-left-20" title="${_('Browse the selected table')}" data-bind="click: function () { setTable(selectedTables()[0]); 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/' + name + '/' + selectedTables()[0].name + '/', $root.sourceType()) : location.href = '/notebook/browse/' + name + '/' + selectedTables()[0].name; }, 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.sourceType()) : location.href = '/notebook/browse/' + selectedTables()[0].catalogEntry.path.join('/'); }, disable: selectedTables().length !== 1">
             <i class="fa fa-play fa-fw"></i> ${_('Query')}
           </button>
           % if has_write_access:
@@ -506,16 +506,16 @@ ${ components.menubar(is_embeddable) }
             <th width="1%">${ _('Type') }</th>
           </tr>
           </thead>
-          <tbody data-bind="hueach: {data: filteredTables, itemHeight: 32, scrollable: '${ MAIN_SCROLLABLE }', scrollableOffset: 277, scrollUp: true}">
+          <tbody data-bind="hueach: { data: filteredTables, itemHeight: 32, scrollable: '${ MAIN_SCROLLABLE }', scrollableOffset: 277, scrollUp: true }">
             <tr>
               <td width="1%" style="text-align: center">
                 <div class="hueCheckbox fa" data-bind="multiCheck: '#tablesTable', value: $data, hueChecked: $parent.selectedTables"></div>
               </td>
               <td width="1%"><a class="blue" href="javascript:void(0)" data-bind="click: showContextPopover"><i class="fa fa-fw fa-info" title="${_('Show details')}"></i></a></td>
               <td>
-                <a class="tableLink" href="javascript:void(0);" data-bind="text: name, click: function() { $parent.setTable($data, function(){ huePubSub.publish('metastore.url.change'); }) }"></a>
+                <a class="tableLink" href="javascript:void(0);" data-bind="text: catalogEntry.name, click: function() { $parent.setTable($data, function() { huePubSub.publish('metastore.url.change'); }) }"></a>
               </td>
-              <td style="text-overflow: ellipsis; overflow: hidden; max-width: 0" data-bind="html: commentWithoutNewLines, attr: {title: hueUtils.html2text(commentWithoutNewLines())}"></td>
+              <td style="text-overflow: ellipsis; overflow: hidden; max-width: 0" data-bind="html: commentWithoutNewLines, attr: { title: hueUtils.html2text(commentWithoutNewLines()) }"></td>
               <!-- ko if: $root.optimizerEnabled -->
                 <!-- ko if: optimizerStats() -->
                 <td>
@@ -524,7 +524,7 @@ ${ components.menubar(is_embeddable) }
                   </div>
                 </td>
                 <td data-bind="text: optimizerStats().column_count"></td>
-              <!-- /ko -->
+                <!-- /ko -->
               <!-- ko ifnot: optimizerStats() -->
                 <td></td>
                 <td></td>
@@ -533,10 +533,10 @@ ${ components.menubar(is_embeddable) }
 
               <td class="center">
                 <!-- ko ifnot: $root.optimizerEnabled && optimizerStats() -->
-                  <!-- ko if: type == 'Table' -->
+                  <!-- ko if: catalogEntry.isTable() -->
                     <i class="fa fa-fw fa-table muted" title="${ _('Table') }"></i>
                   <!-- /ko -->
-                  <!-- ko if: type == 'View' -->
+                  <!-- ko if: catalogEntry.isView() -->
                     <i class="fa fa-fw fa-eye muted" title="${ _('View') }"></i>
                   <!-- /ko -->
                 <!-- /ko -->
@@ -552,19 +552,19 @@ ${ components.menubar(is_embeddable) }
             </tr>
           </tbody>
         </table>
-        <span data-bind="visible: filteredTables().length === 0, css: {'margin-left-10': tables().length > 0}" style="font-style: italic; display: none;">${_('No tables found.')}</span>
+        <span data-bind="visible: filteredTables().length === 0, css: { 'margin-left-10': tables().length > 0 }" style="font-style: italic; display: none;">${_('No tables found.')}</span>
       </div>
     </div>
 
   % if has_write_access:
     <div id="dropTable" class="modal hide fade">
       % if is_embeddable:
-        <form data-bind="attr: { 'action': '/metastore/tables/drop/' + 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="start_time" value=""/>
           <input type="hidden" name="source_type" data-bind="value: $root.sourceType"/>
       % else:
-        <form data-bind="attr: { 'action': '/metastore/tables/drop/' + name }" method="POST">
+        <form data-bind="attr: { 'action': '/metastore/tables/drop/' +catalogEntry. name }" method="POST">
       % endif
         ${ csrf_token(request) | n,unicode }
         <div class="modal-header">
@@ -575,7 +575,7 @@ ${ components.menubar(is_embeddable) }
           <ul data-bind="foreach: selectedTables">
             <!-- ko if: $index() <= 9 -->
             <li>
-              <span data-bind="text: name"></span>
+              <span data-bind="text: catalogEntry.name"></span>
             </li>
             <!-- /ko -->
           </ul>
@@ -591,7 +591,7 @@ ${ components.menubar(is_embeddable) }
           <input type="submit" class="btn btn-danger" value="${_('Yes')}"/>
         </div>
         <!-- ko foreach: selectedTables -->
-        <input type="hidden" name="table_selection" data-bind="value: name" />
+        <input type="hidden" name="table_selection" data-bind="value: catalogEntry.name" />
         <!-- /ko -->
       </form>
     </div>
@@ -629,12 +629,12 @@ ${ components.menubar(is_embeddable) }
     <a class="inactive-action" href="javascript:void(0)" data-bind="tooltip: { placement: 'bottom', delay: 750 }, click: refresh" title="${_('Refresh')}"><i class="pointer fa fa-refresh" data-bind="css: { 'fa-spin blue' : $root.reloading }"></i></a>
     % if has_write_access:
       % if is_embeddable:
-        <a class="inactive-action margin-left-10" data-bind="tooltip: { placement: 'bottom', delay: 750 }, click: function () { huePubSub.publish('open.link', '${ url('indexer:importer_prefill', source_type='all', target_type='table') }' + database().name ); }" title="${_('Create a new table')}" href="javascript:void(0)"><i class="fa fa-plus"></i></a>
+        <a class="inactive-action margin-left-10" data-bind="tooltip: { placement: 'bottom', delay: 750 }, click: function () { huePubSub.publish('open.link', '${ url('indexer:importer_prefill', source_type='all', target_type='table') }' + database().catalogEntry.name ); }" title="${_('Create a new table')}" href="javascript:void(0)"><i class="fa fa-plus"></i></a>
       % elif ENABLE_NEW_CREATE_TABLE.get():
-        <a class="inactive-action margin-left-10" data-bind="tooltip: { placement: 'bottom', delay: 750 }, attr: { 'href': '${ url('indexer:importer_prefill', source_type='all', target_type='table') }' + database().name }" title="${_('Create a new table')}"><i class="fa fa-plus"></i></a>
+        <a class="inactive-action margin-left-10" data-bind="tooltip: { placement: 'bottom', delay: 750 }, attr: { 'href': '${ url('indexer:importer_prefill', source_type='all', target_type='table') }' + database().catalogEntry.name }" title="${_('Create a new table')}"><i class="fa fa-plus"></i></a>
       % else:
-        <a class="inactive-action margin-left-10" data-bind="tooltip: { placement: 'bottom', delay: 750 }, attr: { 'href': '/beeswax/create/import_wizard/' + database().name }" title="${_('Create a new table from a file')}"><span class="fa-stack fa-fw" style="width: 1.28571429em"><i class="fa fa-file-o fa-stack-1x"></i><i class="fa fa-plus-circle fa-stack-1x" style="font-size: 14px; margin-left: 5px; margin-top: 6px;"></i></span></a>
-        <a class="inactive-action margin-left-10" data-bind="tooltip: { placement: 'bottom', delay: 750 }, attr: { 'href': '/beeswax/create/create_table/' + database().name }" title="${_('Create a new table manually')}"><i class="fa fa-plus"></i></a>
+        <a class="inactive-action margin-left-10" data-bind="tooltip: { placement: 'bottom', delay: 750 }, attr: { 'href': '/beeswax/create/import_wizard/' + database().catalogEntry.name }" title="${_('Create a new table from a file')}"><span class="fa-stack fa-fw" style="width: 1.28571429em"><i class="fa fa-file-o fa-stack-1x"></i><i class="fa fa-plus-circle fa-stack-1x" style="font-size: 14px; margin-left: 5px; margin-top: 6px;"></i></span></a>
+        <a class="inactive-action margin-left-10" data-bind="tooltip: { placement: 'bottom', delay: 750 }, attr: { 'href': '/beeswax/create/create_table/' + database().catalogEntry.name }" title="${_('Create a new table manually')}"><i class="fa fa-plus"></i></a>
       % endif
     % endif
   </div>
@@ -646,13 +646,13 @@ ${ components.menubar(is_embeddable) }
     <!-- ko with: table -->
     % if USE_NEW_EDITOR.get():
     <!-- ko if: IS_HUE_4 -->
-      <a class="inactive-action" data-bind="tooltip: { placement: 'bottom', delay: 750 }, click: function() { queryAndWatch('/notebook/browse/' + database.name + '/' + name + '/', $root.sourceType()); }" title="${_('Query the table')}" href="javascript:void(0)"><i class="fa fa-play fa-fw"></i></a>
+      <a class="inactive-action" data-bind="tooltip: { placement: 'bottom', delay: 750 }, click: function() { queryAndWatch('/notebook/browse/' + catalogEntry.path.join('/') + '/', $root.sourceType()); }" title="${_('Query the table')}" href="javascript:void(0)"><i class="fa fa-play fa-fw"></i></a>
     <!-- /ko -->
     <!-- ko if: ! IS_HUE_4 -->
-      <a class="inactive-action" data-bind="tooltip: { placement: 'bottom', delay: 750 }, attr: { 'href': '/notebook/browse/' + database.name + '/' + name }" title="${_('Query the table')}"><i class="fa fa-play fa-fw"></i></a>
+      <a class="inactive-action" data-bind="tooltip: { placement: 'bottom', delay: 750 }, attr: { 'href': '/notebook/browse/' + catalogEntry.path.join('/') }" title="${_('Query the table')}"><i class="fa fa-play fa-fw"></i></a>
     <!-- /ko -->
     % else:
-      <a class="inactive-action" data-bind="tooltip: { placement: 'bottom', delay: 750 }, attr: { 'href': '/metastore/table/'+ database.name + '/' + name + '/read' }" title="${_('Browse Data')}"><i class="fa fa-play fa-fw"></i></a>
+      <a class="inactive-action" data-bind="tooltip: { placement: 'bottom', delay: 750 }, attr: { 'href': '/metastore/table/'+ catalogEntry.path.join('/') + '/read' }" title="${_('Browse Data')}"><i class="fa fa-play fa-fw"></i></a>
     % endif
     <a class="inactive-action" data-bind="tooltip: { placement: 'bottom', delay: 750 }, click: function () { huePubSub.publish('assist.db.refresh', { sourceTypes: [$root.sourceType()] }); }" title="${_('Refresh')}" href="javascript:void(0)"><i class="pointer fa fa-refresh fa-fw" data-bind="css: { 'fa-spin blue' : $root.reloading }"></i></a>
     % if has_write_access:
@@ -674,14 +674,14 @@ ${ components.menubar(is_embeddable) }
     <div class="span3 tile">
       <!-- ko template: 'metastore-table-stats' --><!-- /ko -->
     </div>
-    <!-- ko if: $root.navigatorEnabled() && navigatorStats() -->
+    <!-- ko if: $root.navigatorEnabled() && navigatorMeta() -->
     <div class="span6 tile">
       <h4>${ _('Tags') }</h4>
       <div style="margin-top: 5px" data-bind="component: { name: 'nav-tags', params: {
-        sourceType: $root.sourceType(),
-        database: database.name,
-        table: name
-      } }"></div>
+        sourceType: catalogEntry.dataCatalog.sourceType,
+        database: database.catalogEntry.name,
+        table: catalogEntry.name
+      }}"></div>
     </div>
     <!-- /ko -->
   </div>
@@ -789,13 +789,13 @@ ${ components.menubar(is_embeddable) }
   % if has_write_access:
   <div id="dropPartition" class="modal hide fade">
     % if is_embeddable:
-      <form data-bind="attr: { 'action': '/metastore/table/' + $parent.database.name + '/' + $parent.name + '/partitions/drop' }, submit: dropAndWatch" method="POST">
+      <form data-bind="attr: { 'action': '/metastore/table/' + $parent.catalogEntry.path.join('/') + '/partitions/drop' }, submit: dropAndWatch" method="POST">
         <input type="hidden" name="is_embeddable" value="true"/>
         <input type="hidden" name="format" value="json"/>
         <input type="hidden" name="start_time" value=""/>
         <input type="hidden" name="source_type" data-bind="value: $root.sourceType"/>
     % else:
-      <form data-bind="attr: { 'action': '/metastore/table/' + $parent.database.name + '/' + $parent.name + '/partitions/drop' }" method="POST">
+      <form data-bind="attr: { 'action': '/metastore/table/' + $parent.catalogEntry.path.join('/')+ '/partitions/drop' }" method="POST">
     % endif
       ${ csrf_token(request) | n,unicode }
       <div class="modal-header">
@@ -848,7 +848,7 @@ ${ components.menubar(is_embeddable) }
       <th width="10%">${ _('Impala Compatible') }</th>
     </tr>
     </thead>
-    <tbody data-bind="hueach: {data: $data.optimizerDetails().queryList(), itemHeight: 32, scrollable: '${ MAIN_SCROLLABLE }', scrollableOffset: 200}">
+    <tbody data-bind="hueach: { data: $data.optimizerDetails().queryList(), itemHeight: 32, scrollable: '${ MAIN_SCROLLABLE }', scrollableOffset: 200 }">
     <tr>
       <td data-bind="text: qid"></td>
       <td style="height: 10px; width: 70px; margin-top:5px;" data-bind="attr: {'title': queryCount()}">
@@ -905,12 +905,12 @@ ${ components.menubar(is_embeddable) }
   %endif
 
   <ul class="nav nav-tabs nav-tabs-border margin-top-30">
-    <li><a href="#overview" data-toggle="tab" data-bind="click: function(){ $root.currentTab('table-overview'); }">${_('Overview')}</a></li>
-    <li><a href="#columns" data-toggle="tab" data-bind="click: function(){ $root.currentTab('table-columns'); }">${_('Columns')} (<span data-bind="text: columns().length"></span>)</a></li>
+    <li><a href="#overview" data-toggle="tab" data-bind="click: function() { $root.currentTab('table-overview'); }">${_('Overview')}</a></li>
+    <li><a href="#columns" data-toggle="tab" data-bind="click: function() { $root.currentTab('table-columns'); }">${_('Columns')} (<span data-bind="text: columns().length"></span>)</a></li>
     <!-- ko if: tableDetails() && tableDetails().partition_keys.length -->
-      <li><a href="#partitions" data-toggle="tab" data-bind="click: function(){ $root.currentTab('table-partitions'); }">${_('Partitions')} (<span data-bind="text: partitionsCountLabel"></span>)</a></li>
+      <li><a href="#partitions" data-toggle="tab" data-bind="click: function() { $root.currentTab('table-partitions'); }">${_('Partitions')} (<span data-bind="text: partitionsCountLabel"></span>)</a></li>
     <!-- /ko -->
-    <li><a href="#sample" data-toggle="tab" data-bind="click: function(){ $root.currentTab('table-sample'); }">${_('Sample')}</a></li>
+    <li><a href="#sample" data-toggle="tab" data-bind="click: function() { $root.currentTab('table-sample'); }">${_('Sample')}</a></li>
     <!-- ko if: $root.optimizerEnabled() -->
       <!-- ko if: $root.database().table().optimizerDetails() -->
       ##<li><a href="#permissions" data-toggle="tab" data-bind="click: function(){ $root.currentTab('table-permissions'); }">${_('Permissions')}</a></li>
@@ -974,10 +974,10 @@ ${ components.menubar(is_embeddable) }
             <br>
             server=<span>server1</span>
             <span>
-              <i class="fa fa-long-arrow-right"></i> db=<a class="pointer" title="Browse db privileges"><span data-bind="text: $root.database().name"></span></a>
+              <i class="fa fa-long-arrow-right"></i> db=<a class="pointer" title="Browse db privileges"><span data-bind="text: $root.database().catalogEntry.name"></span></a>
             </span>
             <span>
-              <i class="fa fa-long-arrow-right"></i> table=<a class="pointer" title="Browse table privileges"><span data-bind="text: name"></span></a>
+              <i class="fa fa-long-arrow-right"></i> table=<a class="pointer" title="Browse table privileges"><span data-bind="text: catalogEntry.name"></span></a>
             </span>
             <span style="display: none;">
               <i class="fa fa-long-arrow-right"></i> column=<a class="pointer" title="Browse column privileges"><span></span></a>
@@ -993,10 +993,10 @@ ${ components.menubar(is_embeddable) }
             <br>
             server=server1
             <span>
-              <i class="fa fa-long-arrow-right"></i> db=<a class="pointer" title="Browse db privileges"><span data-bind="text: $root.database().name"></span></a>
+              <i class="fa fa-long-arrow-right"></i> db=<a class="pointer" title="Browse db privileges"><span data-bind="text: $root.database().catalogEntry.name"></span></a>
             </span>
             <span>
-              <i class="fa fa-long-arrow-right"></i> table=<a class="pointer" title="Browse table privileges"><span data-bind="text: name"></span></a>
+              <i class="fa fa-long-arrow-right"></i> table=<a class="pointer" title="Browse table privileges"><span data-bind="text: catalogEntry.name"></span></a>
             </span>
             <span style="display: none;">
               <i class="fa fa-long-arrow-right"></i> column=<a class="pointer" title="Browse column privileges"><span></span></a>
@@ -1028,10 +1028,10 @@ ${ components.menubar(is_embeddable) }
             server=server1
 
               <span>
-                <i class="fa fa-long-arrow-right"></i> db=<a class="pointer" title="Browse db privileges"><span data-bind="text: $root.database().name"></span></a>
+                <i class="fa fa-long-arrow-right"></i> db=<a class="pointer" title="Browse db privileges"><span data-bind="text: $root.database().catalogEntry.name"></span></a>
               </span>
               <span>
-                <i class="fa fa-long-arrow-right"></i> table=<a class="pointer" title="Browse table privileges"><span data-bind="text: name"></span></a>
+                <i class="fa fa-long-arrow-right"></i> table=<a class="pointer" title="Browse table privileges"><span data-bind="text: catalogEntry.name"></span></a>
               </span>
               <span style="display: none;">
                 <i class="fa fa-long-arrow-right"></i> column=<a class="pointer" title="Browse column privileges"><span></span></a>
@@ -1051,7 +1051,7 @@ ${ components.menubar(is_embeddable) }
 
     <div class="tab-pane" id="queries">
       <!-- ko if: $root.optimizerEnabled() && $root.currentTab() == 'table-queries' -->
-        <!-- ko template: {name: 'metastore-queries-tab', data: $root.database().table()} --><!-- /ko -->
+        <!-- ko template: { name: 'metastore-queries-tab', data: $root.database().table() } --><!-- /ko -->
       <!-- /ko -->
     </div>
 
@@ -1071,10 +1071,10 @@ ${ components.menubar(is_embeddable) }
           <tbody data-bind="hueach: {data: $data, itemHeight: 32, scrollable: '${ MAIN_SCROLLABLE }', scrollableOffset: 200}">
           <tr>
             <td data-bind="text: tableEid"></td>
-            <td style="height: 10px; width: 70px; margin-top:5px;" data-bind="attr: {'title': joinpercent()}">
+            <td style="height: 10px; width: 70px; margin-top:5px;" data-bind="attr: { 'title': joinpercent() }">
               <div class="progress bar" style="background-color: #0B7FAD" data-bind="style: { 'width' : joinpercent() + '%' }"></div>
             </td>
-            <td><a data-bind="text: tableName, attr: { href: '/metastore/table/' + $root.database().name + '/' + tableName() }"></a></td>
+            <td><a data-bind="text: tableName, attr: { href: '/metastore/table/' + $root.database().catalogEntry.name + '/' + tableName() }"></a></td>
             <td class="pointer"><code data-bind="text: joinColumns, click: $root.scrollToColumn"></code></td>
             <td data-bind="text: numJoins"></td>
           </tr>
@@ -1225,12 +1225,12 @@ ${ components.menubar(is_embeddable) }
         <h2 class="modal-title">${_('Drop Table')}</h2>
       </div>
       <div class="modal-body">
-        <div>${_('Do you really want to drop the table')} <span style="font-weight: bold;" data-bind="text: database() && database().table() ? database().table().name : ''"></span>?</div>
+        <div>${_('Do you really want to drop the table')} <span style="font-weight: bold;" data-bind="text: database() && database().table() ? database().table().catalogEntry.name : ''"></span>?</div>
       </div>
       <div class="modal-footer">
-        <input type="hidden" name="table_selection" data-bind="value: database() && database().table() ? database().table().name : ''" />
+        <input type="hidden" name="table_selection" data-bind="value: database() && database().table() ? database().table().catalogEntry.name : ''" />
         <input type="button" class="btn" data-dismiss="modal" value="${_('No')}"/>
-        <input type="submit" data-bind="click: function (vm, e) { var $form = $(e.target).parents('form'); $form.attr('action', '/metastore/tables/drop/' + vm.database().name); return true; }" class="btn btn-danger" value="${_('Yes, drop this table')}"/>
+        <input type="submit" data-bind="click: function (vm, e) { var $form = $(e.target).parents('form'); $form.attr('action', '/metastore/tables/drop/' + vm.database().catalogEntry.name); return true; }" class="btn btn-danger" value="${_('Yes, drop this table')}"/>
       </div>
     </form>
   </div>
@@ -1257,7 +1257,7 @@ ${ components.menubar(is_embeddable) }
         $("#dropDatabase").modal('hide');
         $("#dropPartition").modal('hide');
       },
-      error: function (xhr, textStatus, errorThrown) {
+      error: function (xhr) {
         $(document).trigger("error", xhr.responseText);
       }
     });
@@ -1292,7 +1292,6 @@ ${ components.menubar(is_embeddable) }
     });
   }
 
-
   (function () {
     if (ko.options) {
       ko.options.deferUpdates = true;
@@ -1326,9 +1325,9 @@ ${ components.menubar(is_embeddable) }
       }, 'metastore');
 
       viewModel.currentTab.subscribe(function(tab){
-        if (tab == 'table-relationships') {
+        if (tab === 'table-relationships') {
           // viewModel.database().table().getRelationships();
-        } else if (tab == 'table-sample') {
+        } else if (tab === 'table-sample') {
           var selector = '#sample .sample-table';
           % if conf.CUSTOM.BANNER_TOP_HTML.get():
             var bannerTopHeight = 30;
@@ -1397,7 +1396,7 @@ ${ components.menubar(is_embeddable) }
           }, function () {
             var sampleTable = $('#sample .sample-table');
             var sampleCol = sampleTable.find('th').filter(function () {
-              return $.trim($(this).text()).indexOf(col.name()) > -1;
+              return $.trim($(this).text()).indexOf(col.catalogEntry.name) > -1;
             });
             sampleTable.find('.columnSelected').removeClass('columnSelected');
             sampleTable.find('tr td:nth-child(' + (sampleCol.index() + 1) + ')').addClass('columnSelected');
@@ -1410,7 +1409,7 @@ ${ components.menubar(is_embeddable) }
             sampleTable.parent().trigger('scroll_update');
           });
         }
-      }
+      };
 
       ko.applyBindings(viewModel, $('#metastoreComponents')[0]);
 

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

@@ -818,7 +818,7 @@ var DataCatalog = (function () {
   DataCatalogEntry.prototype.getType = function () {
     var self = this;
     var type = self.sourceMeta && self.sourceMeta.type || self.definition.type || '';
-    if (~type.indexOf('<')) {
+    if (type.indexOf('<') !== -1) {
       type = type.substring(0, type.indexOf('<'));
     }
     return type;