Browse Source

HUE-8893 [tb] Extract Table Browser entity models to webpack modules

Johan Ahlen 6 năm trước cách đây
mục cha
commit
55a5b9cf2a

+ 0 - 1094
apps/metastore/src/metastore/static/metastore/js/metastore.model.js

@@ -1,1094 +0,0 @@
-// Licensed to Cloudera, Inc. under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  Cloudera, Inc. licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-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.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();
-      }
-    });
-
-    // When manually changed through dropdown
-    self.namespaceChanged = function (newNamespace, previousNamespace) {
-      if (previousNamespace.database() && !self.namespace().database()) {
-        // Try to set the same database by name, if not there it will revert to 'default'
-        self.namespace().setDatabaseByName(previousNamespace.database().catalogEntry.name, function () {
-          huePubSub.publish('metastore.url.change');
-        });
-      } else {
-        huePubSub.publish('metastore.url.change');
-      }
-    };
-
-    huePubSub.subscribe("assist.db.panel.ready", function () {
-      self.lastLoadNamespacesDeferred.done(function () {
-        var lastSelectedDb = window.apiHelper.getFromTotalStorage('assist_' + self.sourceType + '_' + self.namespace.id, 'lastSelectedDb', 'default');
-        huePubSub.publish('assist.set.database', {
-          source: self.type,
-          namespace: self.namespace().namespace,
-          name: lastSelectedDb
-        });
-      });
-    });
-
-    var getCurrentState = function () {
-      var result = {
-        namespaceId: null,
-        database: null,
-        table: null
-      };
-      var prevNamespaceId = null;
-      var prevDbName = null;
-      var prevTableName = null;
-      if (self.namespace()) {
-        result.namespaceId = self.namespace().id;
-        if (self.namespace().database()) {
-          result.database = self.namespace().database().catalogEntry.name;
-          if (self.namespace().database().table()) {
-            result.table = self.namespace().database().table().catalogEntry.name;
-          }
-        }
-      }
-      return result;
-    };
-
-    var setState = function (state) {
-      if (state.namespaceId) {
-        self.setNamespaceById(state.namespaceId).done(function () {
-          if (state.database) {
-            self.namespace().setDatabaseByName(state.database, function () {
-              if (self.namespace().database() && state.table) {
-                self.namespace().database().setTableByName(state.table);
-              }
-            });
-          }
-        });
-      }
-    };
-
-    var completeRefresh = function (previousState) {
-      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(function () {
-        setState(previousState);
-      }).always(function () {
-        self.reloading(false);
-      });
-    };
-
-    huePubSub.subscribe('context.catalog.namespaces.refreshed', function (sourceType) {
-      if (self.type !== sourceType) {
-        return;
-      }
-      var previousState = getCurrentState();
-      completeRefresh(previousState);
-    });
-
-    huePubSub.subscribe('data.catalog.entry.refreshed', function (details) {
-      var refreshedEntry = details.entry;
-
-      if (refreshedEntry.getSourceType() !== self.type) {
-        return;
-      }
-
-      var previousState = getCurrentState();
-
-      if (refreshedEntry.isSource()) {
-        completeRefresh(previousState);
-      } else if (refreshedEntry.isDatabase() && self.namespace()) {
-        self.namespace().databases().some(function (database) {
-          if (database.catalogEntry === refreshedEntry) {
-            database.load(function () {
-              setState(previousState);
-            }, self.metastoreViewModel.optimizerEnabled(), self.metastoreViewModel.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 (context) {
-      var namespacesWithComputes = context.namespaces.filter(function (namespace) { return namespace.computes.length });
-      self.namespaces($.map(namespacesWithComputes, 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 (options) {
-    var self = this;
-    self.apiHelper = window.apiHelper;
-    self.namespace = options.namespace;
-
-    // TODO: Compute selection in the metastore?
-    self.compute = options.namespace.computes[0];
-    self.id = options.namespace.id;
-    self.name = options.namespace.name;
-    self.metastoreViewModel = options.metastoreViewModel;
-    self.sourceType = options.sourceType;
-    self.navigatorEnabled = options.navigatorEnabled;
-    self.optimizerEnabled = options.optimizerEnabled;
-
-    self.catalogEntry = ko.observable();
-
-    self.database = ko.observable();
-    self.databases = ko.observableArray();
-    self.selectedDatabases = ko.observableArray();
-    self.loading = ko.observable(false);
-    self.lastLoadDatabasesPromise = undefined;
-  };
-
-  MetastoreNamespace.prototype.loadDatabases = function () {
-    var self = this;
-    if (self.loading() && self.lastLoadDatabasesPromise) {
-      return self.lastLoadDatabasesPromise;
-    }
-
-    self.loading(true);
-    var deferred = $.Deferred();
-    self.lastLoadDatabasesPromise = deferred.promise();
-
-    deferred.fail(function () {
-      self.databases([]);
-    }).always(function () {
-      self.loading(false);
-    });
-
-    dataCatalog.getEntry({ namespace: self.namespace, compute: self.compute, sourceType: self.sourceType, path: [], definition: { type: 'source' } }).done(function (entry) {
-      self.catalogEntry(entry);
-      entry.getChildren().done(function (databaseEntries) {
-        self.databases($.map(databaseEntries, function (databaseEntry) {
-          return new MetastoreDatabase({ catalogEntry: databaseEntry, optimizerEnabled: self.optimizerEnabled, metastoreViewModel: self.metastoreViewModel });
-        }));
-        deferred.resolve();
-      }).fail(deferred.reject);
-    });
-
-    return self.lastLoadDatabasesPromise;
-  };
-
-  MetastoreNamespace.prototype.reload = function () {
-    var self = this;
-    if (!self.loading() && self.catalogEntry()) {
-      self.loading(true);
-      // Clear will publish when done
-      self.catalogEntry().clearCache({ invalidate: self.sourceType === 'impala' ? 'invalidate' : 'cache' });
-    }
-  };
-
-  MetastoreNamespace.prototype.setDatabase = function (metastoreDatabase, callback) {
-    var self = this;
-    huePubSub.publish('metastore.scroll.to.top');
-    self.database(metastoreDatabase);
-
-    if (!metastoreDatabase.loaded()) {
-      metastoreDatabase.load(callback, self.optimizerEnabled(), self.navigatorEnabled(), self.sourceType);
-    } else if (callback) {
-      callback();
-    }
-  };
-
-  MetastoreNamespace.prototype.onDatabaseClick = function (catalogEntry) {
-    var self = this;
-
-    self.databases().some(function (database) {
-      if (database.catalogEntry === catalogEntry) {
-        self.setDatabase(database, function() { huePubSub.publish('metastore.url.change') });
-        return true;
-      }
-    });
-  };
-
-  MetastoreNamespace.prototype.setDatabaseByName = function (databaseName, callback) {
-    var self = this;
-
-    var whenLoaded = function (clearCacheOnMissing) {
-      if (!databaseName) {
-        databaseName = self.apiHelper.getFromTotalStorage('editor', 'last.selected.database') ||
-          self.apiHelper.getFromTotalStorage('metastore', 'last.selected.database') || 'default';
-        clearCacheOnMissing = false;
-      }
-      if (self.database() && self.database().catalogEntry.name === databaseName) {
-        if (callback) {
-          callback();
-        }
-        return;
-      }
-      var foundDatabases = self.databases().filter(function (database) {
-        return database.catalogEntry.name === databaseName;
-      });
-
-      if (foundDatabases.length === 1) {
-        self.setDatabase(foundDatabases[0], callback);
-      } else if (clearCacheOnMissing) {
-        self.catalogEntry().clearCache({ invalidate: 'invalidate', silenceErrors: true }).then(function () {
-          self.loadDatabases().done(function () {
-            whenLoaded(false)
-          })
-        })
-      } else {
-        foundDatabases = self.databases().filter(function (database) {
-          return database.catalogEntry.name === 'default';
-        });
-
-        if (foundDatabases.length === 1) {
-          self.setDatabase(foundDatabases[0], callback);
-        } else {
-        }
-      }
-    };
-
-    window.setTimeout(function () {
-      if (self.loading() && self.lastLoadDatabasesPromise !== null) {
-        self.lastLoadDatabasesPromise.done(function () {
-          whenLoaded(true);
-        });
-      } else {
-        if (self.databases().length) {
-          whenLoaded(true);
-        } else {
-          self.loadDatabases().done(function () {
-            whenLoaded(true);
-          })
-        }
-      }
-    }, 0);
-  };
-
-  return MetastoreNamespace;
-})();
-
-var MetastoreDatabase = (function () {
-  /**
-   * @param {object} options
-   * @param {DataCatalogEntry} options.catalogEntry
-   * @param {observable} options.optimizerEnabled
-   * @param {MetastoreViewModel} options.metastoreViewModel;
-   * @constructor
-   */
-  function MetastoreDatabase(options) {
-    var self = this;
-    self.apiHelper = window.apiHelper;
-    self.catalogEntry = options.catalogEntry;
-    self.metastoreViewModel = options.metastoreViewModel;
-
-    self.loaded = ko.observable(false);
-    self.loadingTables = ko.observable(false);
-    self.loadingAnalysis = ko.observable(false);
-    self.loadingComment = ko.observable(false);
-    self.loadingTableComments = ko.observable(false);
-    self.loadingTablePopularity = ko.observable(false);
-
-    self.tables = ko.observableArray();
-
-    self.loading = ko.pureComputed(function () {
-      return self.loadingTables() || self.loadingAnalysis();
-    });
-
-    self.refreshing = ko.pureComputed(function () {
-      return self.loadingTables() || self.loadingAnalysis() || self.loadingComment() || self.loadingTableComments() ||
-        self.loadingTablePopularity();
-    });
-
-    self.comment = ko.observable();
-
-    self.comment.subscribe(function (newValue) {
-      self.catalogEntry.getComment().done(function (comment) {
-        if (comment !== newValue) {
-          self.catalogEntry.setComment(newValue).done(self.comment).fail(function () {
-            self.comment(comment);
-          })
-        }
-      });
-    });
-
-    self.stats = ko.observable();
-    self.navigatorMeta = ko.observable();
-
-    self.showAddTagName = ko.observable(false);
-    self.addTagName = ko.observable('');
-
-    self.selectedTables = ko.observableArray();
-
-    self.editingTable = ko.observable(false);
-    self.table = ko.observable(null);
-  }
-
-  MetastoreDatabase.prototype.onTableClick = function (catalogEntry) {
-    var self = this;
-    self.tables().some(function (table) {
-      if (table.catalogEntry === catalogEntry) {
-        self.setTable(table, function() { huePubSub.publish('metastore.url.change'); });
-        return true;
-      }
-    })
-  };
-
-  MetastoreDatabase.prototype.reload = function () {
-    var self = this;
-    // Clear will publish when done
-    self.catalogEntry.clearCache({ invalidate: self.catalogEntry.getSourceType() === 'impala' ? 'invalidate' : 'cache' });
-  };
-
-  MetastoreDatabase.prototype.load = function (callback, optimizerEnabled, navigatorEnabled) {
-    var self = this;
-
-
-    if (navigatorEnabled) {
-      self.loadingComment(true);
-      self.catalogEntry.getNavigatorMeta().done(self.navigatorMeta).always(function () {
-        self.loadingComment(false);
-      });
-    }
-
-    self.catalogEntry.getComment().done(self.comment);
-
-    self.loadingTables(true);
-    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.loadingTableComments(true);
-        self.catalogEntry.loadNavigatorMetaForChildren().done(function () {
-          self.tables().forEach(function (table) {
-            table.navigatorMeta(table.catalogEntry.navigatorMeta);
-          })
-        }).always(function () {
-          self.loadingTableComments(false);
-        })
-      }
-      if (optimizerEnabled) {
-        self.loadingTablePopularity(true);
-        self.catalogEntry.loadNavOptPopularityForChildren().done(function () {
-          self.tables().forEach(function (table) {
-            table.optimizerStats(table.catalogEntry.navOptPopularity);
-          })
-        }).always(function () {
-          self.loadingTablePopularity(false);
-        })
-      }
-      self.loaded(true);
-    }).fail(function () {
-      self.tables([]);
-    }).always(function () {
-      self.loadingTables(false);
-      if (callback) {
-        callback();
-      }
-    });
-
-    self.loadingAnalysis(true);
-    self.catalogEntry.getAnalysis().done(self.stats).always(function () {
-      self.loadingAnalysis(false);
-    });
-
-    self.apiHelper.setInTotalStorage('metastore', 'last.selected.database', self.name);
-  };
-
-  MetastoreDatabase.prototype.setTableByName = function (tableName) {
-    var self = this;
-
-    if (self.table() && self.table().catalogEntry.name === tableName) {
-      return;
-    }
-
-    var foundTables = self.tables().filter(function (metastoreTable) {
-      return metastoreTable.catalogEntry.name === tableName;
-    });
-
-    if (foundTables.length === 1) {
-      self.setTable(foundTables[0]);
-    }
-  };
-
-  MetastoreDatabase.prototype.setTable = function (metastoreTable, callback) {
-    var self = this;
-    huePubSub.publish('metastore.scroll.to.top');
-    self.table(metastoreTable);
-    if (!metastoreTable.loaded()) {
-      metastoreTable.load();
-    }
-    if (callback) {
-      callback();
-    }
-    self.metastoreViewModel.currentTab('overview');
-  };
-
-  MetastoreDatabase.prototype.showContextPopover = function (entry, event, orientation) {
-    var $source = $(event.currentTarget || event.target);
-    var offset = $source.offset();
-    huePubSub.publish('context.popover.show', {
-      data: {
-        type: 'catalogEntry',
-        catalogEntry: entry.catalogEntry
-      },
-      orientation: orientation || 'right',
-      source: {
-        element: event.target,
-        left: offset.left,
-        top: offset.top - 2,
-        right: offset.left + (orientation === 'left' ? 0 : $source.width() + 1),
-        bottom: offset.top + $source.height() - 2
-      }
-    });
-  };
-
-  return MetastoreDatabase;
-})();
-
-var MetastoreTable = (function () {
-
-  /**
-   * @param {Object} options
-   * @param {MetastoreTable} options.metastoreTable
-   */
-  function MetastoreTablePartitions(options) {
-    var self = this;
-    self.detailedKeys = ko.observableArray();
-    self.keys = ko.observableArray();
-    self.values = ko.observableArray();
-    self.selectedValues = ko.observableArray();
-
-    self.valuesFlat = ko.pureComputed(function(){
-      return self.values().map(function(item){
-        return item.partitionSpec
-      });
-    });
-
-    self.selectedValuesFlat = ko.pureComputed(function(){
-      return self.selectedValues().map(function(item){
-        return item.partitionSpec
-      });
-    });
-
-    self.metastoreTable = options.metastoreTable;
-    self.apiHelper = window.apiHelper;
-
-    self.loaded = ko.observable(false);
-    self.loading = ko.observable(false);
-
-    self.sortDesc = ko.observable(true);
-    self.filters = ko.observableArray([]);
-
-    self.typeaheadValues = function (column) {
-      var values = [];
-      self.values().forEach(function (row) {
-        var cell = row.columns[self.keys().indexOf(column())];
-        if (values.indexOf(cell) !== -1) {
-          values.push(cell);
-        }
-      });
-      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) {
-        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;
-      });
-      postData['sort'] = self.sortDesc() ? 'desc' : 'asc';
-
-      $.ajax({
-        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'
-      });
-    };
-
-    self.preview = {
-      keys: ko.observableArray(),
-      values: ko.observableArray()
-    }
-  }
-
-  MetastoreTablePartitions.prototype.load = function () {
-    var self = this;
-    if (self.loaded()) {
-      return;
-    }
-
-    self.loading(true);
-
-    self.metastoreTable.catalogEntry.getPartitions().done(function (partitions) {
-      self.keys(partitions.partition_keys_json);
-      self.values(partitions.partition_values_json);
-      self.preview.values(self.values().slice(0, 5));
-      self.preview.keys(self.keys());
-      huePubSub.publish('metastore.loaded.partitions');
-    }).always(function () {
-      self.loading(false);
-      self.loaded(true);
-    });
-  };
-
-  /**
-   * @param {Object} options
-   * @param {MetastoreTable} options.metastoreTable
-   */
-  function MetastoreTableSamples(options) {
-    var self = this;
-    self.rows = ko.observableArray();
-    self.headers = ko.observableArray();
-    self.metastoreTable = options.metastoreTable;
-
-    self.hasErrors = ko.observable(false);
-    self.errorMessage = ko.observable();
-    self.loaded = ko.observable(false);
-    self.loading = ko.observable(false);
-
-    self.preview = {
-      headers: ko.observableArray(),
-      rows: ko.observableArray()
-    }
-  }
-
-  MetastoreTableSamples.prototype.load = function () {
-    var self = this;
-    if (self.loaded()) {
-      return;
-    }
-    self.hasErrors(false);
-    self.loading(true);
-    self.metastoreTable.catalogEntry.getSample().done(function (sample) {
-      self.rows(sample.data);
-      self.headers($.map(sample.meta, function (meta) { return meta.name }));
-      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 {DataCatalogEntry} options.catalogEntry
-   * @param {boolean} options.optimizerEnabled
-   * @param {boolean} options.navigatorEnabled
-   * @constructor
-   */
-  function MetastoreTable(options) {
-    var self = this;
-    self.database = options.database;
-    self.optimizerEnabled = options.optimizerEnabled;
-    self.navigatorEnabled = options.navigatorEnabled;
-    self.catalogEntry = options.catalogEntry;
-
-    self.apiHelper = window.apiHelper;
-
-    // TODO: Check if enough or if we need to fetch additional details
-    self.isView = ko.observable(self.catalogEntry.isView());
-    self.viewSql = ko.observable();
-
-    self.optimizerStats = ko.observable();
-    self.optimizerDetails = ko.observable();
-    self.topJoins = ko.observableArray();
-    self.navigatorMeta = ko.observable();
-    self.relationshipsDetails = ko.observable();
-
-    self.loaded = ko.observable(false);
-
-    self.loadingDetails = ko.observable(false);
-    self.loadingColumns = ko.observable(false);
-    self.loadingQueries = ko.observable(false);
-    self.loadingComment = ko.observable(false);
-    self.loadingViewSql = ko.observable(false);
-    self.loadingTopJoins = ko.observable(false);
-
-    self.columns = ko.observableArray();
-
-    self.samples = new MetastoreTableSamples({
-      metastoreTable: self
-    });
-
-    self.partitions = new MetastoreTablePartitions({
-      metastoreTable: self
-    });
-
-    self.loading = ko.pureComputed(function () {
-      return self.loadingDetails() || self.loadingColumns();
-    });
-
-    self.refreshing = ko.pureComputed(function () {
-      return self.loadingDetails() || self.loadingColumns() || self.loadingQueries() || self.loadingComment() ||
-        self.samples.loading() || self.partitions.loading() || self.loadingViewSql() || self.loadingTopJoins();
-    });
-
-    self.partitionsCountLabel = ko.pureComputed(function () {
-      if (self.partitions.values().length === METASTORE_PARTITION_LIMIT) {
-        return self.partitions.values().length + '+'
-      }
-      return self.partitions.values().length;
-    });
-    self.tableDetails = ko.observable();
-    self.tableStats = ko.observable();
-    self.refreshingTableStats = ko.observable(false);
-    self.showAddTagName = ko.observable(false);
-    self.addTagName = ko.observable('');
-
-    self.comment = ko.observable();
-    self.editingComment = ko.observable();
-
-    if (self.catalogEntry.hasResolvedComment()) {
-      self.comment(self.catalogEntry.getResolvedComment());
-    }
-
-    self.commentWithoutNewLines = ko.pureComputed(function(){
-      return self.comment() ? hueUtils.deXSS(self.comment().replace(/[\n\r]+/gi, ' ')) : '';
-    });
-
-    self.comment.subscribe(function (newValue) {
-      self.catalogEntry.getComment().done(function (comment) {
-        if (comment !== newValue) {
-          self.catalogEntry.setComment(newValue).done(self.comment).fail(function () {
-            self.comment(comment);
-          })
-        }
-      });
-    });
-
-    self.refreshTableStats = function () {
-      if (self.refreshingTableStats()) {
-        return;
-      }
-      self.refreshingTableStats(true);
-      self.catalogEntry.getAnalysis({ refreshAnalysis: true, silenceErrors: true }).done(function () {
-        self.fetchDetails();
-      }).fail(function () {
-        self.refreshingTableStats(false);
-        $.jHueNotify.error(window.I18n('An error occurred refreshing the table stats. Please try again.'));
-        console.error('apiHelper.refreshTableStats error');
-        console.error(data);
-      });
-    };
-
-    self.fetchFields = function () {
-      self.loadingColumns(true);
-      self.catalogEntry.getChildren().done(function (columnEntries) {
-        self.columns($.map(columnEntries, function (columnEntry) {
-          return new MetastoreColumn({
-            catalogEntry: columnEntry,
-            table: self
-          })
-        }));
-
-        self.catalogEntry.getNavOptMeta().done(function (navOptMeta) {
-          self.optimizerDetails(navOptMeta);
-
-          var topColIndex = {};
-          navOptMeta.topCols.forEach(function (topCol) {
-            topColIndex[topCol.name] = topCol;
-          });
-
-          self.columns().forEach(function (column) {
-            if (topColIndex[column.catalogEntry.name]) {
-              column.popularity(topColIndex[column.catalogEntry.name].score);
-            }
-          });
-        }).always(function () {
-          self.loadingQueries(false);
-        });
-      }).fail(function () {
-        self.columns([]);
-      }).always(function () {
-        self.loadingColumns(false);
-      });
-    };
-
-    self.fetchDetails = function () {
-      self.loadingComment(true);
-      self.database.catalogEntry.loadNavigatorMetaForChildren().done(function () {
-        self.catalogEntry.getComment().done(self.comment);
-      }).always(function () {
-        self.loadingComment(false);
-      });
-
-      if (self.catalogEntry.isView()) {
-        self.loadingViewSql(true);
-      }
-
-      self.catalogEntry.getTopJoins({ silenceErrors: true }).done(function (topJoins) {
-        if (topJoins && topJoins.values) {
-          var joins = [];
-          var ownQidLower = self.catalogEntry.path.join('.').toLowerCase();
-          var ownNameLower = self.catalogEntry.name.toLowerCase();
-          var ownDbNameLower = self.database.catalogEntry.name.toLowerCase();
-
-          var joinIndex = {};
-          var joinColsIndex = {};
-          topJoins.values.forEach(function (topJoin) {
-            if (topJoin.tables.length === 2) {
-              topJoin.tables.forEach(function (table) {
-                var tableLower = table.toLowerCase();
-                if (tableLower !== ownQidLower && tableLower !== ownNameLower) {
-                  var name = tableLower.indexOf(ownDbNameLower + '.') === 0 ? table.substring(ownDbNameLower.length + 1) : table;
-                  if (!joinIndex[name]) {
-                    joinIndex[name] = {
-                      tableName: name,
-                      tablePath: table.split('.'),
-                      joinCols: [],
-                      queryCount: 0
-                    }
-                  }
-                  var join = joinIndex[name];
-                  join.queryCount += topJoin.totalQueryCount;
-
-                  topJoin.joinCols.forEach(function (joinCol) {
-                    var cleanCols = {
-                      queryCount: topJoin.totalQueryCount
-                    };
-                    if (joinCol.columns.length === 2) {
-                      joinCol.columns.forEach(function (col) {
-                        var colLower = col.toLowerCase();
-                        if (colLower.indexOf(ownQidLower + '.') === 0) {
-                          cleanCols.source = colLower.substring(ownDbNameLower.length + 1);
-                          cleanCols.sourcePath = col.split('.');
-                        } else if (colLower.indexOf(ownNameLower + '.') === 0) {
-                          cleanCols.source = colLower;
-                          cleanCols.sourcePath = col.split('.');
-                          cleanCols.sourcePath.unshift(ownDbNameLower);
-                        } else if (colLower.indexOf(ownDbNameLower + '.') === 0) {
-                          cleanCols.target = colLower.substring(ownDbNameLower.length + 1);
-                          cleanCols.targetPath = col.split('.');
-                        } else {
-                          cleanCols.target = col;
-                          cleanCols.targetPath = col.split('.');
-                        }
-                      })
-                    }
-                    if (cleanCols.source && cleanCols.target) {
-                      if (joinColsIndex[ownQidLower + join.tableName + cleanCols.source + cleanCols.target]) {
-                        joinColsIndex[ownQidLower + join.tableName + cleanCols.source + cleanCols.target].queryCount += topJoin.totalQueryCount;
-                      } else {
-                        joinColsIndex[ownQidLower + join.tableName + cleanCols.source + cleanCols.target] = cleanCols;
-                        join.joinCols.push(cleanCols);
-                      }
-                    }
-                  })
-                }
-              });
-            }
-          });
-
-          Object.keys(joinIndex).forEach(function (key) {
-            var join = joinIndex[key];
-            if (join.joinCols.length) {
-              join.joinCols.sort(function (a, b) {
-                return b.queryCount - a.queryCount;
-              });
-              joins.push(join);
-            }
-          });
-          joins.sort(function (a, b) {
-            return b.queryCount - a.queryCount;
-          });
-          self.topJoins(joins);
-        }
-      }).always(function () {
-        self.loadingTopJoins(false);
-      });
-
-      self.loadingDetails(true);
-      self.catalogEntry.getAnalysis().done(function (analysis) {
-        self.tableDetails(analysis);
-        self.tableStats(analysis.details.stats);
-        self.loaded(true);
-        if (analysis.partition_keys.length) {
-          self.partitions.detailedKeys(analysis.partition_keys);
-          self.partitions.load();
-        } else {
-          self.partitions.loading(false);
-          self.partitions.loaded(true);
-        }
-
-        var found = analysis.properties && analysis.properties.some(function (property) {
-          if (property.col_name.toLowerCase() === 'view original text:') {
-            window.apiHelper.formatSql({ statements: property.data_type }).done(function (formatResponse) {
-              self.viewSql(formatResponse.status === 0 ? formatResponse.formatted_statements : property.data_type)
-            }).fail(function () {
-              self.viewSql(property.data_type)
-            }).always(function () {
-              self.loadingViewSql(false);
-            });
-            return true;
-          }
-        });
-        if (!found) {
-          self.loadingViewSql(false);
-        }
-      }).fail(function () {
-        self.partitions.loading(false);
-        self.partitions.loaded(true);
-        self.loadingViewSql(false);
-      }).always(function () {
-        self.refreshingTableStats(false);
-        self.loadingDetails(false)
-      });
-
-      self.samples.load();
-    };
-
-    self.drop = function () {
-      $.post('/tables/drop/' + self.database.catalogEntry.name, {
-        table_selection: ko.mapping.toJSON([self.name]),
-        skip_trash: 'off',
-        is_embeddable: true,
-        cluster: JSON.stringify(self.database.catalogEntry.compute)
-      }, function(resp) {
-        if (resp.history_uuid) {
-          huePubSub.publish('notebook.task.submitted', resp.history_uuid);
-        } else {
-          $(document).trigger("error", data.message);
-        }
-      });
-    };
-
-    self.getRelationships = function () {
-      $.post('/metadata/api/navigator/lineage', {
-        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) {
-        $(document).trigger("info", xhr.responseText);
-      });
-    };
-  }
-
-  MetastoreTable.prototype.reload = function () {
-    var self = this;
-    self.samples.loaded(false);
-    self.partitions.loaded(false);
-    // Clear will publish when done
-    self.catalogEntry.clearCache({ invalidate: self.catalogEntry.getSourceType() === 'impala' ? 'invalidate' : 'cache' });
-  };
-
-  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.catalogEntry.path.join('/') + '/load?source_type=' + self.catalogEntry.getSourceType(), function (data) {
-      $("#import-data-modal").html(data['data']);
-    }).fail(function (xhr) {
-      $(document).trigger("error", xhr.responseText);
-    });
-  };
-
-  MetastoreTable.prototype.load = function () {
-    var self = this;
-    self.fetchFields();
-    self.fetchDetails();
-    huePubSub.publish('metastore.loaded.table');
-  };
-
-
-  var contextPopoverTimeout = -1;
-
-  MetastoreTable.prototype.showContextPopover = function (entry, event, orientation) {
-    window.clearTimeout(contextPopoverTimeout);
-    var $source = $(event.currentTarget || event.target);
-    var offset = $source.offset();
-    huePubSub.publish('context.popover.show', {
-      data: {
-        type: 'catalogEntry',
-        catalogEntry: entry.catalogEntry
-      },
-      orientation: orientation || 'right',
-      source: {
-        element: event.target,
-        left: offset.left,
-        top: offset.top - 2,
-        right: offset.left + (orientation === 'left' ? 0 : $source.width() + 1),
-        bottom: offset.top + $source.height() - 2
-      }
-    });
-  };
-
-  MetastoreTable.prototype.showContextPopoverDelayed = function (entry, event, orientation) {
-    var self = this;
-    window.clearTimeout(contextPopoverTimeout);
-    contextPopoverTimeout = window.setTimeout(function () {
-      self.showContextPopover(entry, event, orientation);
-    }, 500);
-  };
-
-  MetastoreTable.prototype.clearContextPopoverDelay = function () {
-    window.clearInterval(contextPopoverTimeout);
-  };
-
-  return MetastoreTable;
-
-})();
-
-var MetastoreColumn = (function () {
-
-  /**
-   * @param {Object} options
-   * @param {MetastoreTable} options.table
-   * @param {DataCatalogEntry} options.catalogEntry
-   * @constructor
-   */
-  function MetastoreColumn(options) {
-    var self = this;
-    self.table = options.table;
-    self.catalogEntry = options.catalogEntry;
-
-    self.favourite = ko.observable(false);
-    self.popularity = ko.observable();
-    self.comment = ko.observable();
-
-    self.comment.subscribe(function (newValue) {
-      self.catalogEntry.getComment().done(function (comment) {
-        if (comment !== newValue) {
-          self.catalogEntry.setComment(newValue).done(self.comment).fail(function () {
-            self.comment(comment);
-          })
-        }
-      });
-    });
-
-    self.table.catalogEntry.loadNavigatorMetaForChildren().done(function () {
-      self.catalogEntry.getComment().done(self.comment);
-    });
-  }
-
-  MetastoreColumn.prototype.showContextPopover = function (entry, event) {
-    var $source = $(event.target);
-    var offset = $source.offset();
-    huePubSub.publish('context.popover.show', {
-      data: {
-        type: 'catalogEntry',
-        catalogEntry: entry.catalogEntry
-      },
-      orientation: 'right',
-      source: {
-        element: event.target,
-        left: offset.left,
-        top: offset.top - 2,
-        right: offset.left + $source.width() + 1,
-        bottom: offset.top + $source.height() - 2
-      }
-    });
-  };
-
-  return MetastoreColumn;
-})();

+ 75 - 0
desktop/core/src/desktop/js/apps/table_browser/metastoreColumn.js

@@ -0,0 +1,75 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import huePubSub from 'utils/huePubSub';
+
+class MetastoreColumn {
+  /**
+   * @param {Object} options
+   * @param {MetastoreTable} options.table
+   * @param {DataCatalogEntry} options.catalogEntry
+   * @constructor
+   */
+  constructor(options) {
+    this.table = options.table;
+    this.catalogEntry = options.catalogEntry;
+
+    this.favourite = ko.observable(false);
+    this.popularity = ko.observable();
+    this.comment = ko.observable();
+
+    this.comment.subscribe(newValue => {
+      this.catalogEntry.getComment().done(comment => {
+        if (comment !== newValue) {
+          this.catalogEntry
+            .setComment(newValue)
+            .done(this.comment)
+            .fail(() => {
+              this.comment(comment);
+            });
+        }
+      });
+    });
+
+    this.table.catalogEntry.loadNavigatorMetaForChildren().done(() => {
+      this.catalogEntry.getComment().done(this.comment);
+    });
+  }
+
+  showContextPopover(entry, event) {
+    const $source = $(event.target);
+    const offset = $source.offset();
+    huePubSub.publish('context.popover.show', {
+      data: {
+        type: 'catalogEntry',
+        catalogEntry: entry.catalogEntry
+      },
+      orientation: 'right',
+      source: {
+        element: event.target,
+        left: offset.left,
+        top: offset.top - 2,
+        right: offset.left + $source.width() + 1,
+        bottom: offset.top + $source.height() - 2
+      }
+    });
+  }
+}
+
+export default MetastoreColumn;

+ 223 - 0
desktop/core/src/desktop/js/apps/table_browser/metastoreDatabase.js

@@ -0,0 +1,223 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import apiHelper from 'api/apiHelper';
+import huePubSub from 'utils/huePubSub';
+import MetastoreTable from 'apps/table_browser/metastoreTable';
+
+class MetastoreDatabase {
+  /**
+   * @param {object} options
+   * @param {DataCatalogEntry} options.catalogEntry
+   * @param {observable} options.optimizerEnabled
+   * @param {MetastoreViewModel} options.metastoreViewModel;
+   * @constructor
+   */
+  constructor(options) {
+    this.catalogEntry = options.catalogEntry;
+    this.metastoreViewModel = options.metastoreViewModel;
+
+    this.loaded = ko.observable(false);
+    this.loadingTables = ko.observable(false);
+    this.loadingAnalysis = ko.observable(false);
+    this.loadingComment = ko.observable(false);
+    this.loadingTableComments = ko.observable(false);
+    this.loadingTablePopularity = ko.observable(false);
+
+    this.tables = ko.observableArray();
+
+    this.loading = ko.pureComputed(() => this.loadingTables() || this.loadingAnalysis());
+
+    this.refreshing = ko.pureComputed(
+      () =>
+        this.loadingTables() ||
+        this.loadingAnalysis() ||
+        this.loadingComment() ||
+        this.loadingTableComments() ||
+        this.loadingTablePopularity()
+    );
+
+    this.comment = ko.observable();
+
+    this.comment.subscribe(newValue => {
+      this.catalogEntry.getComment().done(comment => {
+        if (comment !== newValue) {
+          this.catalogEntry
+            .setComment(newValue)
+            .done(this.comment)
+            .fail(() => {
+              this.comment(comment);
+            });
+        }
+      });
+    });
+
+    this.stats = ko.observable();
+    this.navigatorMeta = ko.observable();
+
+    this.showAddTagName = ko.observable(false);
+    this.addTagName = ko.observable('');
+
+    this.selectedTables = ko.observableArray();
+
+    this.editingTable = ko.observable(false);
+    this.table = ko.observable(null);
+  }
+
+  onTableClick(catalogEntry) {
+    this.tables().some(table => {
+      if (table.catalogEntry === catalogEntry) {
+        this.setTable(table, () => {
+          huePubSub.publish('metastore.url.change');
+        });
+        return true;
+      }
+    });
+  }
+
+  reload() {
+    // Clear will publish when done
+    this.catalogEntry.clearCache({
+      invalidate: this.catalogEntry.getSourceType() === 'impala' ? 'invalidate' : 'cache'
+    });
+  }
+
+  load(callback, optimizerEnabled, navigatorEnabled) {
+    if (navigatorEnabled) {
+      this.loadingComment(true);
+      this.catalogEntry
+        .getNavigatorMeta()
+        .done(this.navigatorMeta)
+        .always(() => {
+          this.loadingComment(false);
+        });
+    }
+
+    this.catalogEntry.getComment().done(this.comment);
+
+    this.loadingTables(true);
+    this.catalogEntry
+      .getChildren()
+      .done(tableEntries => {
+        this.tables(
+          tableEntries.map(
+            tableEntry =>
+              new MetastoreTable({
+                database: this,
+                catalogEntry: tableEntry,
+                optimizerEnabled: optimizerEnabled,
+                navigatorEnabled: navigatorEnabled
+              })
+          )
+        );
+        if (navigatorEnabled) {
+          this.loadingTableComments(true);
+          this.catalogEntry
+            .loadNavigatorMetaForChildren()
+            .done(() => {
+              this.tables().forEach(table => {
+                table.navigatorMeta(table.catalogEntry.navigatorMeta);
+              });
+            })
+            .always(() => {
+              this.loadingTableComments(false);
+            });
+        }
+        if (optimizerEnabled) {
+          this.loadingTablePopularity(true);
+          this.catalogEntry
+            .loadNavOptPopularityForChildren()
+            .done(() => {
+              this.tables().forEach(table => {
+                table.optimizerStats(table.catalogEntry.navOptPopularity);
+              });
+            })
+            .always(() => {
+              this.loadingTablePopularity(false);
+            });
+        }
+        this.loaded(true);
+      })
+      .fail(() => {
+        this.tables([]);
+      })
+      .always(() => {
+        this.loadingTables(false);
+        if (callback) {
+          callback();
+        }
+      });
+
+    this.loadingAnalysis(true);
+    this.catalogEntry
+      .getAnalysis()
+      .done(this.stats)
+      .always(() => {
+        this.loadingAnalysis(false);
+      });
+
+    apiHelper.setInTotalStorage('metastore', 'last.selected.database', this.name);
+  }
+
+  setTableByName(tableName) {
+    if (this.table() && this.table().catalogEntry.name === tableName) {
+      return;
+    }
+
+    this.tables().some(metastoreTable => {
+      if (metastoreTable.catalogEntry.name === tableName) {
+        this.setTable(metastoreTable);
+        return true;
+      }
+    });
+  }
+
+  setTable(metastoreTable, callback) {
+    huePubSub.publish('metastore.scroll.to.top');
+    this.table(metastoreTable);
+    if (!metastoreTable.loaded()) {
+      metastoreTable.load();
+    }
+    if (callback) {
+      callback();
+    }
+    this.metastoreViewModel.currentTab('overview');
+  }
+
+  showContextPopover(entry, event, orientation) {
+    const $source = $(event.currentTarget || event.target);
+    const offset = $source.offset();
+    huePubSub.publish('context.popover.show', {
+      data: {
+        type: 'catalogEntry',
+        catalogEntry: entry.catalogEntry
+      },
+      orientation: orientation || 'right',
+      source: {
+        element: event.target,
+        left: offset.left,
+        top: offset.top - 2,
+        right: offset.left + (orientation === 'left' ? 0 : $source.width() + 1),
+        bottom: offset.top + $source.height() - 2
+      }
+    });
+  }
+}
+
+export default MetastoreDatabase;

+ 189 - 0
desktop/core/src/desktop/js/apps/table_browser/metastoreNamespace.js

@@ -0,0 +1,189 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import apiHelper from 'api/apiHelper';
+import dataCatalog from 'catalog/dataCatalog';
+import huePubSub from 'utils/huePubSub';
+import MetastoreDatabase from 'apps/table_browser/metastoreDatabase';
+
+class MetastoreNamespace {
+  constructor(options) {
+    this.namespace = options.namespace;
+
+    // TODO: Compute selection in the metastore?
+    this.compute = options.namespace.computes[0];
+    this.id = options.namespace.id;
+    this.name = options.namespace.name;
+    this.metastoreViewModel = options.metastoreViewModel;
+    this.sourceType = options.sourceType;
+    this.navigatorEnabled = options.navigatorEnabled;
+    this.optimizerEnabled = options.optimizerEnabled;
+
+    this.catalogEntry = ko.observable();
+
+    this.database = ko.observable();
+    this.databases = ko.observableArray();
+    this.selectedDatabases = ko.observableArray();
+    this.loading = ko.observable(false);
+    this.lastLoadDatabasesPromise = undefined;
+  }
+
+  loadDatabases() {
+    if (this.loading() && this.lastLoadDatabasesPromise) {
+      return this.lastLoadDatabasesPromise;
+    }
+
+    this.loading(true);
+    const deferred = $.Deferred();
+    this.lastLoadDatabasesPromise = deferred.promise();
+
+    deferred
+      .fail(() => {
+        this.databases([]);
+      })
+      .always(() => {
+        this.loading(false);
+      });
+
+    dataCatalog
+      .getEntry({
+        namespace: this.namespace,
+        compute: this.compute,
+        sourceType: this.sourceType,
+        path: [],
+        definition: { type: 'source' }
+      })
+      .done(entry => {
+        this.catalogEntry(entry);
+        entry
+          .getChildren()
+          .done(databaseEntries => {
+            this.databases(
+              databaseEntries.map(
+                databaseEntry =>
+                  new MetastoreDatabase({
+                    catalogEntry: databaseEntry,
+                    optimizerEnabled: this.optimizerEnabled,
+                    metastoreViewModel: this.metastoreViewModel
+                  })
+              )
+            );
+            deferred.resolve();
+          })
+          .fail(deferred.reject);
+      });
+
+    return this.lastLoadDatabasesPromise;
+  }
+
+  reload() {
+    if (!this.loading() && this.catalogEntry()) {
+      this.loading(true);
+      // Clear will publish when done
+      this.catalogEntry().clearCache({
+        invalidate: this.sourceType === 'impala' ? 'invalidate' : 'cache'
+      });
+    }
+  }
+
+  setDatabase(metastoreDatabase, callback) {
+    huePubSub.publish('metastore.scroll.to.top');
+    this.database(metastoreDatabase);
+
+    if (!metastoreDatabase.loaded()) {
+      metastoreDatabase.load(
+        callback,
+        this.optimizerEnabled(),
+        this.navigatorEnabled(),
+        this.sourceType
+      );
+    } else if (callback) {
+      callback();
+    }
+  }
+
+  onDatabaseClick(catalogEntry) {
+    this.databases().some(database => {
+      if (database.catalogEntry === catalogEntry) {
+        this.setDatabase(database, () => {
+          huePubSub.publish('metastore.url.change');
+        });
+        return true;
+      }
+    });
+  }
+
+  setDatabaseByName(databaseName, callback) {
+    const whenLoaded = clearCacheOnMissing => {
+      if (!databaseName) {
+        databaseName =
+          apiHelper.getFromTotalStorage('editor', 'last.selected.database') ||
+          apiHelper.getFromTotalStorage('metastore', 'last.selected.database') ||
+          'default';
+        clearCacheOnMissing = false;
+      }
+      if (this.database() && this.database().catalogEntry.name === databaseName) {
+        if (callback) {
+          callback();
+        }
+        return;
+      }
+      let foundDatabases = this.databases().filter(
+        database => database.catalogEntry.name === databaseName
+      );
+
+      if (foundDatabases.length === 1) {
+        this.setDatabase(foundDatabases[0], callback);
+      } else if (clearCacheOnMissing) {
+        this.catalogEntry()
+          .clearCache({ invalidate: 'invalidate', silenceErrors: true })
+          .then(() => {
+            this.loadDatabases().done(() => {
+              whenLoaded(false);
+            });
+          });
+      } else {
+        foundDatabases = this.databases().filter(
+          database => database.catalogEntry.name === 'default'
+        );
+
+        if (foundDatabases.length === 1) {
+          this.setDatabase(foundDatabases[0], callback);
+        } else {
+        }
+      }
+    };
+
+    window.setTimeout(() => {
+      if (this.loading() && this.lastLoadDatabasesPromise !== null) {
+        this.lastLoadDatabasesPromise.done(() => {
+          whenLoaded(true);
+        });
+      } else if (this.databases().length) {
+        whenLoaded(true);
+      } else {
+        this.loadDatabases().done(() => {
+          whenLoaded(true);
+        });
+      }
+    }, 0);
+  }
+}
+
+export default MetastoreNamespace;

+ 241 - 0
desktop/core/src/desktop/js/apps/table_browser/metastoreSource.js

@@ -0,0 +1,241 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import apiHelper from 'api/apiHelper';
+import contextCatalog from 'catalog/contextCatalog';
+import huePubSub from 'utils/huePubSub';
+import MetastoreNamespace from 'apps/table_browser/metastoreNamespace';
+
+class MetastoreSource {
+  constructor(options) {
+    this.type = options.type;
+    this.name = options.name;
+    this.metastoreViewModel = options.metastoreViewModel;
+
+    this.reloading = ko.observable(false);
+    this.loading = ko.observable(false);
+
+    this.lastLoadNamespacesDeferred = $.Deferred();
+    this.namespace = ko.observable();
+    this.namespaces = ko.observableArray();
+
+    this.namespace.subscribe(() => {
+      if (this.namespace() && this.namespace().databases().length === 0) {
+        this.namespace().loadDatabases();
+      }
+    });
+
+    // When manually changed through dropdown
+    this.namespaceChanged = (newNamespace, previousNamespace) => {
+      if (previousNamespace.database() && !this.namespace().database()) {
+        // Try to set the same database by name, if not there it will revert to 'default'
+        this.namespace().setDatabaseByName(previousNamespace.database().catalogEntry.name, () => {
+          huePubSub.publish('metastore.url.change');
+        });
+      } else {
+        huePubSub.publish('metastore.url.change');
+      }
+    };
+
+    huePubSub.subscribe('assist.db.panel.ready', () => {
+      this.lastLoadNamespacesDeferred.done(() => {
+        const lastSelectedDb = apiHelper.getFromTotalStorage(
+          'assist_' + this.sourceType + '_' + this.namespace.id,
+          'lastSelectedDb',
+          'default'
+        );
+        huePubSub.publish('assist.set.database', {
+          source: this.type,
+          namespace: this.namespace().namespace,
+          name: lastSelectedDb
+        });
+      });
+    });
+
+    const getCurrentState = () => {
+      const result = {
+        namespaceId: null,
+        database: null,
+        table: null
+      };
+      if (this.namespace()) {
+        result.namespaceId = this.namespace().id;
+        if (this.namespace().database()) {
+          result.database = this.namespace().database().catalogEntry.name;
+          if (
+            this.namespace()
+              .database()
+              .table()
+          ) {
+            result.table = this.namespace()
+              .database()
+              .table().catalogEntry.name;
+          }
+        }
+      }
+      return result;
+    };
+
+    const setState = state => {
+      if (state.namespaceId) {
+        this.setNamespaceById(state.namespaceId).done(() => {
+          if (state.database) {
+            this.namespace().setDatabaseByName(state.database, () => {
+              if (this.namespace().database() && state.table) {
+                this.namespace()
+                  .database()
+                  .setTableByName(state.table);
+              }
+            });
+          }
+        });
+      }
+    };
+
+    const completeRefresh = previousState => {
+      this.reloading(true);
+      if (
+        this.namespace() &&
+        this.namespace().database() &&
+        this.namespace()
+          .database()
+          .table()
+      ) {
+        this.namespace()
+          .database()
+          .table(null);
+      }
+      if (this.namespace() && this.namespace().database()) {
+        this.namespace().database(null);
+      }
+      if (this.namespace()) {
+        this.namespace(null);
+      }
+      this.loadNamespaces()
+        .done(() => {
+          setState(previousState);
+        })
+        .always(() => {
+          this.reloading(false);
+        });
+    };
+
+    huePubSub.subscribe('context.catalog.namespaces.refreshed', sourceType => {
+      if (this.type !== sourceType) {
+        return;
+      }
+      const previousState = getCurrentState();
+      completeRefresh(previousState);
+    });
+
+    huePubSub.subscribe('data.catalog.entry.refreshed', details => {
+      const refreshedEntry = details.entry;
+
+      if (refreshedEntry.getSourceType() !== this.type) {
+        return;
+      }
+
+      const previousState = getCurrentState();
+
+      if (refreshedEntry.isSource()) {
+        completeRefresh(previousState);
+      } else if (refreshedEntry.isDatabase() && this.namespace()) {
+        this.namespace()
+          .databases()
+          .some(function(database) {
+            if (database.catalogEntry === refreshedEntry) {
+              database.load(
+                () => {
+                  setState(previousState);
+                },
+                this.metastoreViewModel.optimizerEnabled(),
+                this.metastoreViewModel.navigatorEnabled()
+              );
+              return true;
+            }
+          });
+      } else if (refreshedEntry.isTableOrView()) {
+        this.namespace()
+          .databases()
+          .some(database => {
+            if (database.catalogEntry.name === refreshedEntry.path[0]) {
+              database.tables().some(table => {
+                if (table.catalogEntry.name === refreshedEntry.name) {
+                  table.load();
+                  return true;
+                }
+              });
+              return true;
+            }
+          });
+      }
+    });
+  }
+
+  loadNamespaces() {
+    this.loading(true);
+    contextCatalog
+      .getNamespaces({ sourceType: this.type })
+      .done(context => {
+        const namespacesWithComputes = context.namespaces.filter(
+          namespace => namespace.computes.length
+        );
+        this.namespaces(
+          namespacesWithComputes.map(
+            namespace =>
+              new MetastoreNamespace({
+                metastoreViewModel: this.metastoreViewModel,
+                sourceType: this.type,
+                navigatorEnabled: this.metastoreViewModel.navigatorEnabled,
+                optimizerEnabled: this.metastoreViewModel.optimizerEnabled,
+                namespace: namespace
+              })
+          )
+        );
+        this.namespace(this.namespaces()[0]);
+        this.lastLoadNamespacesDeferred.resolve();
+      })
+      .fail(this.lastLoadNamespacesDeferred.reject)
+      .always(() => {
+        this.loading(false);
+      });
+    return this.lastLoadNamespacesDeferred;
+  }
+
+  setNamespaceById(namespaceId) {
+    const deferred = $.Deferred();
+    this.lastLoadNamespacesDeferred
+      .done(() => {
+        const found = this.namespaces().some(namespace => {
+          if (namespace.namespace.id === namespaceId) {
+            this.namespace(namespace);
+            deferred.resolve();
+            return true;
+          }
+        });
+        if (!found) {
+          deferred.reject();
+        }
+      })
+      .fail(deferred.reject);
+    return deferred.promise();
+  }
+}
+
+export default MetastoreSource;

+ 447 - 0
desktop/core/src/desktop/js/apps/table_browser/metastoreTable.js

@@ -0,0 +1,447 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import apiHelper from 'api/apiHelper';
+import huePubSub from 'utils/huePubSub';
+import hueUtils from 'utils/hueUtils';
+import MetastoreColumn from 'apps/table_browser/metastoreColumn';
+import MetastoreTableSamples from 'apps/table_browser/metastoreTableSamples';
+import MetastoreTablePartitions from 'apps/table_browser/metastoreTablePartitions';
+import I18n from 'utils/i18n';
+
+let contextPopoverTimeout = -1;
+
+class MetastoreTable {
+  /**
+   * @param {Object} options
+   * @param {MetastoreDatabase} options.database
+   * @param {DataCatalogEntry} options.catalogEntry
+   * @param {boolean} options.optimizerEnabled
+   * @param {boolean} options.navigatorEnabled
+   * @constructor
+   */
+  constructor(options) {
+    this.database = options.database;
+    this.optimizerEnabled = options.optimizerEnabled;
+    this.navigatorEnabled = options.navigatorEnabled;
+    this.catalogEntry = options.catalogEntry;
+
+    // TODO: Check if enough or if we need to fetch additional details
+    this.isView = ko.observable(this.catalogEntry.isView());
+    this.viewSql = ko.observable();
+
+    this.optimizerStats = ko.observable();
+    this.optimizerDetails = ko.observable();
+    this.topJoins = ko.observableArray();
+    this.navigatorMeta = ko.observable();
+    this.relationshipsDetails = ko.observable();
+
+    this.loaded = ko.observable(false);
+
+    this.loadingDetails = ko.observable(false);
+    this.loadingColumns = ko.observable(false);
+    this.loadingQueries = ko.observable(false);
+    this.loadingComment = ko.observable(false);
+    this.loadingViewSql = ko.observable(false);
+    this.loadingTopJoins = ko.observable(false);
+
+    this.columns = ko.observableArray();
+
+    this.samples = new MetastoreTableSamples({
+      metastoreTable: this
+    });
+
+    this.partitions = new MetastoreTablePartitions({
+      metastoreTable: this
+    });
+
+    this.loading = ko.pureComputed(() => this.loadingDetails() || this.loadingColumns());
+
+    this.refreshing = ko.pureComputed(
+      () =>
+        this.loadingDetails() ||
+        this.loadingColumns() ||
+        this.loadingQueries() ||
+        this.loadingComment() ||
+        this.samples.loading() ||
+        this.partitions.loading() ||
+        this.loadingViewSql() ||
+        this.loadingTopJoins()
+    );
+
+    this.partitionsCountLabel = ko.pureComputed(() => {
+      if (this.partitions.values().length === window.METASTORE_PARTITION_LIMIT) {
+        return this.partitions.values().length + '+';
+      }
+      return this.partitions.values().length;
+    });
+    this.tableDetails = ko.observable();
+    this.tableStats = ko.observable();
+    this.refreshingTableStats = ko.observable(false);
+    this.showAddTagName = ko.observable(false);
+    this.addTagName = ko.observable('');
+
+    this.comment = ko.observable();
+    this.editingComment = ko.observable();
+
+    if (this.catalogEntry.hasResolvedComment()) {
+      this.comment(this.catalogEntry.getResolvedComment());
+    }
+
+    this.commentWithoutNewLines = ko.pureComputed(() =>
+      this.comment() ? hueUtils.deXSS(this.comment().replace(/[\n\r]+/gi, ' ')) : ''
+    );
+
+    this.comment.subscribe(newValue => {
+      this.catalogEntry.getComment().done(comment => {
+        if (comment !== newValue) {
+          this.catalogEntry
+            .setComment(newValue)
+            .done(this.comment)
+            .fail(() => {
+              this.comment(comment);
+            });
+        }
+      });
+    });
+
+    this.refreshTableStats = () => {
+      if (this.refreshingTableStats()) {
+        return;
+      }
+      this.refreshingTableStats(true);
+      this.catalogEntry
+        .getAnalysis({ refreshAnalysis: true, silenceErrors: true })
+        .done(() => {
+          this.fetchDetails();
+        })
+        .fail(data => {
+          this.refreshingTableStats(false);
+          $.jHueNotify.error(
+            I18n('An error occurred refreshing the table stats. Please try again.')
+          );
+          console.error('apiHelper.refreshTableStats error');
+          console.error(data);
+        });
+    };
+
+    this.fetchFields = () => {
+      this.loadingColumns(true);
+      this.catalogEntry
+        .getChildren()
+        .done(columnEntries => {
+          this.columns(
+            columnEntries.map(
+              columnEntry =>
+                new MetastoreColumn({
+                  catalogEntry: columnEntry,
+                  table: this
+                })
+            )
+          );
+
+          this.catalogEntry
+            .getNavOptMeta()
+            .done(navOptMeta => {
+              this.optimizerDetails(navOptMeta);
+
+              const topColIndex = {};
+              navOptMeta.topCols.forEach(topCol => {
+                topColIndex[topCol.name] = topCol;
+              });
+
+              this.columns().forEach(column => {
+                if (topColIndex[column.catalogEntry.name]) {
+                  column.popularity(topColIndex[column.catalogEntry.name].score);
+                }
+              });
+            })
+            .always(() => {
+              this.loadingQueries(false);
+            });
+        })
+        .fail(() => {
+          this.columns([]);
+        })
+        .always(() => {
+          this.loadingColumns(false);
+        });
+    };
+
+    this.fetchDetails = () => {
+      this.loadingComment(true);
+      this.database.catalogEntry
+        .loadNavigatorMetaForChildren()
+        .done(() => {
+          this.catalogEntry.getComment().done(this.comment);
+        })
+        .always(() => {
+          this.loadingComment(false);
+        });
+
+      if (this.catalogEntry.isView()) {
+        this.loadingViewSql(true);
+      }
+
+      this.catalogEntry
+        .getTopJoins({ silenceErrors: true })
+        .done(topJoins => {
+          if (topJoins && topJoins.values) {
+            const joins = [];
+            const ownQidLower = this.catalogEntry.path.join('.').toLowerCase();
+            const ownNameLower = this.catalogEntry.name.toLowerCase();
+            const ownDbNameLower = this.database.catalogEntry.name.toLowerCase();
+            const joinIndex = {};
+            const joinColsIndex = {};
+
+            topJoins.values.forEach(topJoin => {
+              if (topJoin.tables.length === 2) {
+                topJoin.tables.forEach(table => {
+                  const tableLower = table.toLowerCase();
+                  if (tableLower !== ownQidLower && tableLower !== ownNameLower) {
+                    const name =
+                      tableLower.indexOf(ownDbNameLower + '.') === 0
+                        ? table.substring(ownDbNameLower.length + 1)
+                        : table;
+                    if (!joinIndex[name]) {
+                      joinIndex[name] = {
+                        tableName: name,
+                        tablePath: table.split('.'),
+                        joinCols: [],
+                        queryCount: 0
+                      };
+                    }
+                    const join = joinIndex[name];
+                    join.queryCount += topJoin.totalQueryCount;
+
+                    topJoin.joinCols.forEach(joinCol => {
+                      const cleanCols = {
+                        queryCount: topJoin.totalQueryCount
+                      };
+                      if (joinCol.columns.length === 2) {
+                        joinCol.columns.forEach(col => {
+                          const colLower = col.toLowerCase();
+                          if (colLower.indexOf(ownQidLower + '.') === 0) {
+                            cleanCols.source = colLower.substring(ownDbNameLower.length + 1);
+                            cleanCols.sourcePath = col.split('.');
+                          } else if (colLower.indexOf(ownNameLower + '.') === 0) {
+                            cleanCols.source = colLower;
+                            cleanCols.sourcePath = col.split('.');
+                            cleanCols.sourcePath.unshift(ownDbNameLower);
+                          } else if (colLower.indexOf(ownDbNameLower + '.') === 0) {
+                            cleanCols.target = colLower.substring(ownDbNameLower.length + 1);
+                            cleanCols.targetPath = col.split('.');
+                          } else {
+                            cleanCols.target = col;
+                            cleanCols.targetPath = col.split('.');
+                          }
+                        });
+                      }
+                      if (cleanCols.source && cleanCols.target) {
+                        if (
+                          joinColsIndex[
+                            ownQidLower + join.tableName + cleanCols.source + cleanCols.target
+                          ]
+                        ) {
+                          joinColsIndex[
+                            ownQidLower + join.tableName + cleanCols.source + cleanCols.target
+                          ].queryCount += topJoin.totalQueryCount;
+                        } else {
+                          joinColsIndex[
+                            ownQidLower + join.tableName + cleanCols.source + cleanCols.target
+                          ] = cleanCols;
+                          join.joinCols.push(cleanCols);
+                        }
+                      }
+                    });
+                  }
+                });
+              }
+            });
+
+            Object.keys(joinIndex).forEach(key => {
+              const join = joinIndex[key];
+              if (join.joinCols.length) {
+                join.joinCols.sort((a, b) => b.queryCount - a.queryCount);
+                joins.push(join);
+              }
+            });
+            joins.sort((a, b) => b.queryCount - a.queryCount);
+            this.topJoins(joins);
+          }
+        })
+        .always(() => {
+          this.loadingTopJoins(false);
+        });
+
+      this.loadingDetails(true);
+      this.catalogEntry
+        .getAnalysis()
+        .done(analysis => {
+          this.tableDetails(analysis);
+          this.tableStats(analysis.details.stats);
+          this.loaded(true);
+          if (analysis.partition_keys.length) {
+            this.partitions.detailedKeys(analysis.partition_keys);
+            this.partitions.load();
+          } else {
+            this.partitions.loading(false);
+            this.partitions.loaded(true);
+          }
+
+          const found =
+            analysis.properties &&
+            analysis.properties.some(property => {
+              if (property.col_name.toLowerCase() === 'view original text:') {
+                apiHelper
+                  .formatSql({ statements: property.data_type })
+                  .done(formatResponse => {
+                    this.viewSql(
+                      formatResponse.status === 0
+                        ? formatResponse.formatted_statements
+                        : property.data_type
+                    );
+                  })
+                  .fail(() => {
+                    this.viewSql(property.data_type);
+                  })
+                  .always(() => {
+                    this.loadingViewSql(false);
+                  });
+                return true;
+              }
+            });
+          if (!found) {
+            this.loadingViewSql(false);
+          }
+        })
+        .fail(() => {
+          this.partitions.loading(false);
+          this.partitions.loaded(true);
+          this.loadingViewSql(false);
+        })
+        .always(() => {
+          this.refreshingTableStats(false);
+          this.loadingDetails(false);
+        });
+
+      this.samples.load();
+    };
+
+    this.drop = () => {
+      $.post('/tables/drop/' + this.database.catalogEntry.name, {
+        table_selection: ko.mapping.toJSON([this.database.catalogEntry.name]),
+        skip_trash: 'off',
+        is_embeddable: true,
+        cluster: JSON.stringify(this.database.catalogEntry.compute)
+      }).done(resp => {
+        if (resp.history_uuid) {
+          huePubSub.publish('notebook.task.submitted', resp.history_uuid);
+        } else {
+          $(document).trigger('error', resp.message);
+        }
+      });
+    };
+
+    this.getRelationships = () => {
+      $.post('/metadata/api/navigator/lineage', {
+        id: this.navigatorMeta().identity
+      })
+        .done(data => {
+          if (data && data.status === 0) {
+            this.relationshipsDetails(ko.mapping.fromJS(data));
+          } else {
+            $(document).trigger('error', data.message);
+          }
+        })
+        .fail(xhr => {
+          $(document).trigger('info', xhr.responseText);
+        });
+    };
+  }
+
+  reload() {
+    this.samples.loaded(false);
+    this.partitions.loaded(false);
+    // Clear will publish when done
+    this.catalogEntry.clearCache({
+      invalidate: this.catalogEntry.getSourceType() === 'impala' ? 'invalidate' : 'cache'
+    });
+  }
+
+  showImportData() {
+    $('#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/' +
+        this.catalogEntry.path.join('/') +
+        '/load?source_type=' +
+        this.catalogEntry.getSourceType()
+    )
+      .done(data => {
+        $('#import-data-modal').html(data['data']);
+      })
+      .fail(xhr => {
+        $(document).trigger('error', xhr.responseText);
+      });
+  }
+
+  load() {
+    this.fetchFields();
+    this.fetchDetails();
+    huePubSub.publish('metastore.loaded.table');
+  }
+
+  showContextPopover(entry, event, orientation) {
+    window.clearTimeout(contextPopoverTimeout);
+    const $source = $(event.currentTarget || event.target);
+    const offset = $source.offset();
+    huePubSub.publish('context.popover.show', {
+      data: {
+        type: 'catalogEntry',
+        catalogEntry: entry.catalogEntry
+      },
+      orientation: orientation || 'right',
+      source: {
+        element: event.target,
+        left: offset.left,
+        top: offset.top - 2,
+        right: offset.left + (orientation === 'left' ? 0 : $source.width() + 1),
+        bottom: offset.top + $source.height() - 2
+      }
+    });
+  }
+
+  showContextPopoverDelayed(entry, event, orientation) {
+    window.clearTimeout(contextPopoverTimeout);
+    contextPopoverTimeout = window.setTimeout(() => {
+      this.showContextPopover(entry, event, orientation);
+    }, 500);
+  }
+
+  clearContextPopoverDelay() {
+    window.clearInterval(contextPopoverTimeout);
+  }
+}
+
+export default MetastoreTable;

+ 122 - 0
desktop/core/src/desktop/js/apps/table_browser/metastoreTablePartitions.js

@@ -0,0 +1,122 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+import komapping from 'knockout.mapping';
+
+import huePubSub from 'utils/huePubSub';
+
+class MetastoreTablePartitions {
+  /**
+   * @param {Object} options
+   * @param {MetastoreTable} options.metastoreTable
+   */
+  constructor(options) {
+    this.detailedKeys = ko.observableArray();
+    this.keys = ko.observableArray();
+    this.values = ko.observableArray();
+    this.selectedValues = ko.observableArray();
+
+    this.valuesFlat = ko.pureComputed(() => this.values().map(item => item.partitionSpec));
+
+    this.selectedValuesFlat = ko.pureComputed(() =>
+      this.selectedValues().map(item => item.partitionSpec)
+    );
+
+    this.metastoreTable = options.metastoreTable;
+
+    this.loaded = ko.observable(false);
+    this.loading = ko.observable(false);
+
+    this.sortDesc = ko.observable(true);
+    this.filters = ko.observableArray([]);
+
+    this.typeaheadValues = column => {
+      const values = [];
+      this.values().forEach(row => {
+        const cell = row.columns[this.keys().indexOf(column())];
+        if (values.indexOf(cell) !== -1) {
+          values.push(cell);
+        }
+      });
+      return values;
+    };
+
+    this.addFilter = () => {
+      this.filters.push(komapping.fromJS({ column: '', value: '' }));
+    };
+
+    this.removeFilter = data => {
+      this.filters.remove(data);
+      if (this.filters().length === 0) {
+        this.sortDesc(true);
+        this.filter();
+      }
+    };
+
+    this.filter = () => {
+      this.loading(true);
+      this.loaded(false);
+      const filters = JSON.parse(ko.toJSON(this.filters));
+      const postData = {};
+      filters.forEach(filter => {
+        postData[filter.column] = filter.value;
+      });
+      postData['sort'] = this.sortDesc() ? 'desc' : 'asc';
+
+      $.ajax({
+        type: 'POST',
+        url: '/metastore/table/' + this.metastoreTable.catalogEntry.path.join('/') + '/partitions',
+        data: postData,
+        dataType: 'json'
+      }).done(data => {
+        this.values(data.partition_values_json);
+        this.loading(false);
+        this.loaded(true);
+      });
+    };
+
+    this.preview = {
+      keys: ko.observableArray(),
+      values: ko.observableArray()
+    };
+  }
+
+  load() {
+    if (this.loaded()) {
+      return;
+    }
+
+    this.loading(true);
+
+    this.metastoreTable.catalogEntry
+      .getPartitions()
+      .done(partitions => {
+        this.keys(partitions.partition_keys_json);
+        this.values(partitions.partition_values_json);
+        this.preview.values(this.values().slice(0, 5));
+        this.preview.keys(this.keys());
+        huePubSub.publish('metastore.loaded.partitions');
+      })
+      .always(() => {
+        this.loading(false);
+        this.loaded(true);
+      });
+  }
+}
+
+export default MetastoreTablePartitions;

+ 65 - 0
desktop/core/src/desktop/js/apps/table_browser/metastoreTableSamples.js

@@ -0,0 +1,65 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import ko from 'knockout';
+
+class MetastoreTableSamples {
+  /**
+   * @param {Object} options
+   * @param {MetastoreTable} options.metastoreTable
+   */
+  constructor(options) {
+    this.rows = ko.observableArray();
+    this.headers = ko.observableArray();
+    this.metastoreTable = options.metastoreTable;
+
+    this.hasErrors = ko.observable(false);
+    this.errorMessage = ko.observable();
+    this.loaded = ko.observable(false);
+    this.loading = ko.observable(false);
+
+    this.preview = {
+      headers: ko.observableArray(),
+      rows: ko.observableArray()
+    };
+  }
+
+  load() {
+    if (this.loaded()) {
+      return;
+    }
+    this.hasErrors(false);
+    this.loading(true);
+    this.metastoreTable.catalogEntry
+      .getSample()
+      .done(sample => {
+        this.rows(sample.data);
+        this.headers(sample.meta.map(meta => meta.name));
+        this.preview.rows(this.rows().slice(0, 3));
+        this.preview.headers(this.headers());
+      })
+      .fail(message => {
+        this.errorMessage(message);
+        this.hasErrors(true);
+      })
+      .always(() => {
+        this.loading(false);
+        this.loaded(true);
+      });
+  }
+}
+
+export default MetastoreTableSamples;