Browse Source

HUE-7917 [frontend] New improved context popover for SQL entities

Johan Ahlen 7 năm trước cách đây
mục cha
commit
d508358e07
25 tập tin đã thay đổi với 1331 bổ sung1256 xóa
  1. 1 1
      apps/beeswax/src/beeswax/server/dbms.py
  2. 25 10
      apps/metastore/src/metastore/static/metastore/js/metastore.model.js
  3. 4 2
      apps/metastore/src/metastore/templates/metastore.mako
  4. 0 0
      desktop/core/src/desktop/static/desktop/css/hue-embedded.css
  5. 0 0
      desktop/core/src/desktop/static/desktop/css/hue.css
  6. 0 0
      desktop/core/src/desktop/static/desktop/css/hue3-extra.css
  7. 0 143
      desktop/core/src/desktop/static/desktop/css/hue3.css
  8. 6 6
      desktop/core/src/desktop/static/desktop/ext/css/selectize.css
  9. 146 24
      desktop/core/src/desktop/static/desktop/js/apiHelper.js
  10. 3 20
      desktop/core/src/desktop/static/desktop/js/assist/assistDbEntry.js
  11. 49 4
      desktop/core/src/desktop/static/desktop/js/dataCatalog.js
  12. 17 1
      desktop/core/src/desktop/static/desktop/js/ko.hue-bindings.js
  13. 461 0
      desktop/core/src/desktop/static/desktop/less/components/hue-popover.less
  14. 9 3
      desktop/core/src/desktop/static/desktop/less/components/hue-selectize.less
  15. 0 180
      desktop/core/src/desktop/static/desktop/less/hue-assist.less
  16. 1 0
      desktop/core/src/desktop/static/desktop/less/hue-cross-version.less
  17. 20 174
      desktop/core/src/desktop/static/desktop/less/hue4.less
  18. 2 0
      desktop/core/src/desktop/templates/ko_components.mako
  19. 333 0
      desktop/core/src/desktop/templates/ko_components/ko_catalog_entries_table.mako
  20. 228 670
      desktop/core/src/desktop/templates/ko_components/ko_context_popover.mako
  21. 1 1
      desktop/core/src/desktop/templates/ko_components/ko_global_search.mako
  22. 9 7
      desktop/core/src/desktop/templates/ko_components/ko_inline_autocomplete.mako
  23. 10 6
      desktop/core/src/desktop/templates/ko_components/ko_nav_tags.mako
  24. 5 3
      desktop/libs/notebook/src/notebook/connectors/hiveserver2.py
  25. 1 1
      desktop/libs/notebook/src/notebook/static/notebook/js/notebook.ko.js

+ 1 - 1
apps/beeswax/src/beeswax/server/dbms.py

@@ -88,7 +88,7 @@ def get_query_server_config(name='beeswax', server=None):
     kerberos_principal = hive_site.get_hiveserver2_kerberos_principal(HIVE_SERVER_HOST.get())
 
     query_server = {
-        'server_name': 'beeswax',
+        'server_name': name,
         'server_host': HIVE_SERVER_HOST.get(),
         'server_port': HIVE_SERVER_PORT.get(),
         'principal': kerberos_principal,

+ 25 - 10
apps/metastore/src/metastore/static/metastore/js/metastore.model.js

@@ -175,6 +175,25 @@ var MetastoreDatabase = (function () {
     });
   };
 
+  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;
 })();
 
@@ -311,8 +330,8 @@ var MetastoreTable = (function () {
     self.hasErrors(false);
     self.loading(true);
     self.metastoreTable.catalogEntry.getSample().done(function (sample) {
-      self.rows(sample.rows);
-      self.headers(sample.headers);
+      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) {
@@ -567,12 +586,10 @@ var MetastoreTable = (function () {
     var offset = $source.offset();
     huePubSub.publish('context.popover.show', {
       data: {
-        type: 'table',
-        identifierChain: [{ name: entry.database.catalogEntry.name }, { name: entry.catalogEntry.name }]
+        type: 'catalogEntry',
+        catalogEntry: entry.catalogEntry
       },
       orientation: orientation || 'right',
-      sourceType: entry.catalogEntry.getSourceType(),
-      defaultDatabase: entry.database.catalogEntry.name,
       source: {
         element: event.target,
         left: offset.left,
@@ -637,12 +654,10 @@ var MetastoreColumn = (function () {
     var offset = $source.offset();
     huePubSub.publish('context.popover.show', {
       data: {
-        type: 'column',
-        identifierChain: [{ name: entry.table.database.catalogEntry.name }, { name: entry.table.catalogEntry.name }, { name: entry.catalogEntry.name }]
+        type: 'catalogEntry',
+        catalogEntry: entry.catalogEntry
       },
       orientation: 'right',
-      sourceType: entry.catalogEntry.getSourceType(),
-      defaultDatabase: entry.table.database.catalogEntry.name,
       source: {
         element: event.target,
         left: offset.left,

+ 4 - 2
apps/metastore/src/metastore/templates/metastore.mako

@@ -265,7 +265,7 @@ ${ components.menubar(is_embeddable) }
         <tr>
           <td data-bind="text: $index() + 1"></td>
           <!-- ko foreach: $data -->
-            <td data-bind="text: $data"></td>
+            <td data-bind="html: $data"></td>
           <!-- /ko -->
         </tr>
       <!-- /ko -->
@@ -412,6 +412,7 @@ ${ components.menubar(is_embeddable) }
     <thead>
     <tr>
       <th width="1%" style="text-align: center" class="vertical-align-middle"><div class="hueCheckbox fa" data-bind="hueCheckAll: { allValues: filteredDatabases, selectedValues: selectedDatabases }"></div></th>
+      <th>&nbsp;</th>
       <th>${ _('Database Name') }</th>
     </tr>
     </thead>
@@ -420,6 +421,7 @@ ${ components.menubar(is_embeddable) }
       <td width="1%" style="text-align: center">
         <div class="hueCheckbox fa" data-bind="multiCheck: '#databasesTable', value: $data, hueChecked: $parent.selectedDatabases"></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 href="javascript: void(0);" data-bind="text: catalogEntry.name, click: function () { $parent.setDatabase($data, function() { huePubSub.publish('metastore.url.change'); }) }"></a>
       </td>
@@ -466,7 +468,7 @@ ${ components.menubar(is_embeddable) }
             % endif
           </div>
         </div>
-        <div class="span6 tile">
+        <div class="span5 tile">
           <!-- ko if: $root.navigatorEnabled()  -->
           <h4>${ _('Tags') }</h4>
           <div style="margin-top: 5px" data-bind="component: { name: 'nav-tags', params: {

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
desktop/core/src/desktop/static/desktop/css/hue-embedded.css


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
desktop/core/src/desktop/static/desktop/css/hue.css


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
desktop/core/src/desktop/static/desktop/css/hue3-extra.css


+ 0 - 143
desktop/core/src/desktop/static/desktop/css/hue3.css

@@ -791,149 +791,6 @@ body {
   box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18), 0 2px 8px 0 rgba(0, 0, 0, 0.13);
 }
 
-.hue-popover {
-  position: absolute;
-  display: -ms-flexbox;
-  display: flex;
-  -ms-flex-direction: column;
-  flex-direction: column;
-  top: 0;
-  left: 0;
-  z-index: 1060;
-  padding: 1px;
-  background-color: #fff;
-  -webkit-background-clip: padding-box;
-  background-clip: padding-box;
-  border: 1px solid transparent;
-}
-
-.hue-popover.hue-popover-top {
-  margin-top: -5px;
-}
-
-.hue-popover.hue-popover-top .hue-popover-arrow {
-  bottom: -6px;
-  left: 50%;
-  margin-left: -6px;
-  border-top-color: #0B7FAD;
-  border-bottom-width: 0;
-}
-
-.hue-popover.hue-popover-top .hue-popover-arrow::after {
-  bottom: 1px;
-  margin-left: -3px;
-  content: "";
-  border-top-color: #0B7FAD;
-  border-bottom-width: 0;
-}
-
-.hue-popover.hue-popover-right {
-  margin-left: 5px;
-}
-
-.hue-popover.hue-popover-right .hue-popover-arrow {
-  top: 50%;
-  left: -6px;
-  margin-top: -6px;
-  border-right-color: #0B7FAD;
-  border-left-width: 0;
-}
-
-.hue-popover.hue-popover-right .hue-popover-arrow::after {
-  bottom: -3px;
-  left: 1px;
-  content: "";
-  border-right-color: #0B7FAD;
-  border-left-width: 0;
-}
-
-.hue-popover.hue-popover-bottom {
-  margin-top: 7px;
-}
-
-.hue-popover.hue-popover-bottom .hue-popover-arrow {
-  top: -6px;
-  left: 50%;
-  margin-left: -6px;
-  border-top-width: 0;
-  border-bottom-color: #0B7FAD;
-}
-
-.hue-popover.hue-popover-bottom .hue-popover-arrow::after {
-  top: 3px;
-  margin-left: -3px;
-  content: "";
-  border-top-width: 0;
-  border-bottom-color: #0B7FAD;
-}
-
-.hue-popover.hue-popover-left {
-  margin-left: -5px;
-}
-
-.hue-popover.hue-popover-left .hue-popover-arrow {
-  top: 50%;
-  right: -6px;
-  margin-top: -3px;
-  border-right-width: 0;
-  border-left-color: #0B7FAD;
-}
-
-.hue-popover.hue-popover-left .hue-popover-arrow::after {
-  right: 2px;
-  bottom: -3px;
-  content: "";
-  border-right-width: 0;
-  border-left-color: #0B7FAD;
-}
-
-.hue-popover-title {
-  -ms-flex: 0 1 auto;
-  flex: 0 1 auto;
-  padding: 6px 10px;
-  margin: 0;
-  font-size: 0.9rem;
-  background-color: #fff;
-}
-
-.hue-popover-title:empty {
-  display: none;
-}
-
-.hue-popover-arrow, .hue-popover-arrow::after {
-  position: absolute;
-  display: block;
-  width: 0;
-  height: 0;
-  border-color: transparent;
-  border-style: solid;
-}
-
-.hue-popover-arrow {
-  border-width: 6px;
-}
-
-.hue-popover-top {
-  border-radius: 2px;
-  background-color: #FFF;
-  -webkit-box-shadow: 0 -2px 8px rgba(0,0,0,.18);
-  box-shadow: 0 -2px 6px 0 rgba(0, 0, 0, 0.18), 0 -2px 8px 0 rgba(0, 0, 0, 0.13);
-}
-
-.hue-popover-right {
-  border-radius: 2px;
-  background-color: #FFF;
-  -webkit-box-shadow: 2px 0 8px rgba(0,0,0,.18);
-  box-shadow: 2px 0 6px 0 rgba(0, 0, 0, 0.18), 2px 0 8px 0 rgba(0, 0, 0, 0.13);
-}
-
-.hue-popover-left {
-  border-radius: 2px;
-  background-color: #FFF;
-  -webkit-box-shadow: -2px 0 8px rgba(0,0,0,.18);
-  box-shadow: -2px 0 6px 0 rgba(0, 0, 0, 0.18), -2px 0 8px 0 rgba(0, 0, 0, 0.13);
-}
-
 .hue-inner-drop-down {
   margin: 0 !important;
   padding: 0;

+ 6 - 6
desktop/core/src/desktop/static/desktop/ext/css/selectize.css

@@ -232,12 +232,12 @@
 
 .selectize-control.multi .selectize-input > div {
   cursor: pointer;
-  margin: 0 3px 3px 0;
-  padding: 2px 6px;
-  background: #0B7FAD;
-  color: #FFF;
-  border: 0 solid #0B7FAD;
-  border-radius: 5px
+  margin: 0 5px 5px 0;
+  padding: 2px 10px;
+  background: #E7E7E7;
+  color: #424242;
+  border: 0 solid #E7E7E7;
+  border-radius: 12px
 }
 
 .selectize-read-only.multi .selectize-input > div {

+ 146 - 24
desktop/core/src/desktop/static/desktop/js/apiHelper.js

@@ -85,7 +85,7 @@ var CancellablePromise = (function () {
       ApiHelper.getInstance().cancelActiveRequest(self.request);
     }
 
-    if (self.state() === 'pending' && self.deferred.reject) {
+    if (self.state && self.state() === 'pending' && self.deferred.reject) {
       self.deferred.reject();
     }
 
@@ -157,6 +157,7 @@ var ApiHelper = (function () {
   var SOLR_FIELDS_API = '/indexer/api/index/list/';
   var DASHBOARD_TERMS_API = '/dashboard/get_terms';
   var DASHBOARD_STATS_API = '/dashboard/get_stats';
+  var FORMAT_SQL_API = '/notebook/api/format';
 
   var SEARCH_API = '/desktop/api/search/entities';
   var INTERACTIVE_SEARCH_API = '/desktop/api/search/entities_interactive';
@@ -371,12 +372,12 @@ var ApiHelper = (function () {
         }
       }
 
-      if (! options.silenceErrors) {
+      if (!options || !options.silenceErrors) {
         hueUtils.logError(errorResponse);
         $(document).trigger("error", errorMessage);
       }
 
-      if (options.errorCallback) {
+      if (options && options.errorCallback) {
         options.errorCallback(errorMessage);
       }
       return errorMessage;
@@ -402,7 +403,7 @@ var ApiHelper = (function () {
     return $.post(url, data, function (data) {
       if (self.successResponseIsError(data)) {
         self.assistErrorCallback(options)(data);
-      } else if (typeof options.successCallback !== 'undefined') {
+      } else if (options && options.successCallback) {
         options.successCallback(data);
       }
     })
@@ -971,13 +972,14 @@ var ApiHelper = (function () {
    * @param {Function} [options.errorCallback]
    * @param {boolean} [options.silenceErrors]
    * @param {boolean} [options.fetchContents]
-   *
    * @param {number} options.uuid
+   *
+   * @return {CancellablePromise}
    */
   ApiHelper.prototype.fetchDocument = function (options) {
     var self = this;
-    var promise = $.Deferred();
-    $.ajax({
+    var deferred = $.Deferred();
+    var request = $.ajax({
       url: DOCUMENTS_API,
       data: {
         uuid: options.uuid,
@@ -985,18 +987,18 @@ var ApiHelper = (function () {
       },
       success: function (data) {
         if (! self.successResponseIsError(data)) {
-          promise.resolve(data)
+          deferred.resolve(data)
         } else {
-          promise.reject(self.assistErrorCallback({
+          deferred.reject(self.assistErrorCallback({
             silenceErrors: options.silenceErrors
           }));
         }
       }
     })
     .fail(function (errorResponse) {
-      promise.reject(self.assistErrorHandler(errorResponse))
+      deferred.reject(self.assistErrorHandler(errorResponse))
     });
-    return promise;
+    return new CancellablePromise(deferred, request);
   };
 
   /**
@@ -1451,6 +1453,55 @@ var ApiHelper = (function () {
     return new CancellablePromise(deferred, undefined, promises);
   };
 
+  /**
+   * Checks the status for the given snippet ID
+   *
+   * @param {Object} options
+   * @param {Object} options.notebookJson
+   * @param {Object} options.snippetJson
+   * @param {boolean} [options.silenceErrors]
+   *
+   * @return {CancellablePromise}
+   */
+  ApiHelper.prototype.whenAvailable = function (options) {
+    var self = this;
+    var deferred = $.Deferred();
+    var cancellablePromises = [];
+
+    var waitTimeout = -1;
+
+    deferred.fail(function () {
+      window.clearTimeout(waitTimeout);
+    });
+
+    var waitForAvailable = function () {
+      var request = self.simplePost('/notebook/api/check_status', {
+        notebook: options.notebookJson,
+        snippet: options.snippetJson
+      }, {
+        silenceErrors: options.silenceErrors
+      }).done(function (response) {
+        if (response && response.query_status && response.query_status.status) {
+          var status = response.query_status.status;
+          if (status === 'available') {
+            deferred.resolve();
+          } else if (status === 'running' || status === 'starting' || status === 'waiting') {
+            waitTimeout = window.setTimeout(function () {
+              waitForAvailable();
+            }, 500);
+          } else {
+            deferred.reject();
+          }
+        }
+      }).fail(deferred.reject);
+
+      cancellablePromises.push(new CancellablePromise(request, request));
+    };
+
+    waitForAvailable();
+    return new CancellablePromise(deferred, undefined, cancellablePromises);
+  };
+
   /**
    * Fetches samples for the given source and path
    *
@@ -1458,6 +1509,7 @@ var ApiHelper = (function () {
    * @param {boolean} [options.silenceErrors]
    *
    * @param {string} options.sourceType
+   * @param {number} [options.sampleCount] - Default 100
    * @param {string[]} options.path
    *
    * @return {CancellablePromise}
@@ -1466,21 +1518,76 @@ var ApiHelper = (function () {
     var self = this;
     var deferred = $.Deferred();
 
-    var request = self.simplePost(SAMPLE_API_PREFIX + options.path.join('/'), {
+    var cancellablePromises = [];
+
+    var snippetJson;
+    var notebookJson;
+
+    var cancelled = false;
+
+    var cancelQuery = function () {
+      cancelled = true;
+      if (snippetJson) {
+        self.simplePost('/notebook/api/cancel_statement', {
+          notebook: notebookJson || {},
+          snippet: snippetJson
+        }, { silenceErrors:  options.silenceErrors });
+      }
+    };
+
+    self.simplePost(SAMPLE_API_PREFIX + options.path.join('/'), {
       notebook: {},
       snippet: ko.mapping.toJSON({
         type: options.sourceType
-      })
+      }),
+      async: true
     }, {
-      silenceErrors: options.silenceErrors,
-      successCallback: function (data) {
-        data.hueTimestamp = Date.now();
-        deferred.resolve(data);
-      },
-      errorCallback: deferred.reject
-    }).done().fail(deferred.reject);
+      silenceErrors: options.silenceErrors
+    }).done(function (sampleResponse) {
+      if (sampleResponse.history_uuid) {
+        self.fetchDocument({
+          uuid: sampleResponse.history_uuid,
+          fetchContents: true
+        }).done(function (docResponse) {
+          if (docResponse.data && docResponse.data.snippets && docResponse.data.snippets.length === 1) {
+            notebookJson = JSON.stringify(docResponse.data);
+            snippetJson = JSON.stringify(docResponse.data.snippets[0]);
+
+            // The promise might get cancelled before we've received the document needed to actually cancel it
+            if (cancelled) {
+              cancelQuery();
+              return;
+            }
 
-    return new CancellablePromise(deferred, request);
+            cancellablePromises.push(self.whenAvailable({ notebookJson: notebookJson, snippetJson: snippetJson, silenceErrors: options.silenceErrors }).done(function () {
+              var resultRequest = self.simplePost('/notebook/api/fetch_result_data', {
+                notebook: notebookJson,
+                snippet: snippetJson,
+                rows: options.sampleCount || 100,
+                startOver: 'false'
+              }, {
+                silenceErrors:  options.silenceErrors
+              }).done(function (sampleResponse) {
+                var data = (sampleResponse && sampleResponse.result) || { data: [], meta: [] };
+                data.hueTimestamp = Date.now();
+                deferred.resolve(data);
+              }).fail(deferred.reject);
+              cancellablePromises.push(resultRequest, resultRequest);
+            }).fail(deferred.reject));
+          } else {
+            deferred.reject();
+          }
+        }).fail(deferred.reject);
+      } else {
+        deferred.reject();
+      }
+    }).fail(deferred.reject);
+
+    cancellablePromises.push({
+      cancel: cancelQuery
+    });
+
+    return new CancellablePromise(deferred, undefined, cancellablePromises);
   };
 
   /**
@@ -1787,10 +1894,25 @@ var ApiHelper = (function () {
     return new CancellablePromise(deferred, request);
   };
 
-  ApiHelper.prototype.formatSql = function (statements) {
-    return $.post("/notebook/api/format", {
-      statements: statements
+  /**
+   *
+   * @param {Object} options
+   * @param {string} options.statements
+   * @param {boolean} [options.silenceErrors]
+   */
+  ApiHelper.prototype.formatSql = function (options) {
+    var self = this;
+    var deferred = $.Deferred();
+
+    var request = self.simplePost(FORMAT_SQL_API, {
+      statements: options.statements
+    }, {
+      silenceErrors: options.silenceErrors,
+      successCallback: deferred.resolve,
+      errorCallback: deferred.reject
     });
+
+    return new CancellablePromise(deferred, request);
   };
 
   /**

+ 3 - 20
desktop/core/src/desktop/static/desktop/js/assist/assistDbEntry.js

@@ -197,31 +197,14 @@ var AssistDbEntry = (function () {
       offset.top += positionAdjustment.top;
     }
 
-    var type;
-    if (self.catalogEntry.isColumn() && self.sourceType === 'solr') {
-      type = 'collection';
-    } else if (self.catalogEntry.isColumn()) {
-      type = 'column';
-    } else if (self.catalogEntry.isDatabase()) {
-      type = 'database';
-    } else if (self.catalogEntry.isTable()) {
-      type = 'table';
-    } else if (self.catalogEntry.isView()) {
-      type = 'view';
-    } else {
-      type = 'complex';
-    }
-
     self.statsVisible(true);
     huePubSub.publish('context.popover.show', {
       data: {
-        type: type,
-        identifierChain: $.map(self.getHierarchy(), function (name) { return { name: name }})
+        type: 'catalogEntry',
+        catalogEntry: self.catalogEntry
       },
-      showInAssistEnabled: self.navigationSettings.rightAssist,
+      showInAssistEnabled: !!self.navigationSettings.rightAssist,
       orientation: self.navigationSettings.rightAssist ? 'left' : 'right',
-      sourceType: self.sourceType,
-      defaultDatabase: self.databaseName,
       pinEnabled: self.navigationSettings.pinEnabled,
       source: {
         element: event.target,

+ 49 - 4
desktop/core/src/desktop/static/desktop/js/dataCatalog.js

@@ -489,7 +489,11 @@ var DataCatalog = (function () {
      */
     var reloadNavigatorMeta = function (dataCatalogEntry, apiOptions) {
       if (dataCatalogEntry.canHaveNavigatorMetadata()) {
-        return dataCatalogEntry.trackedPromise('navigatorMetaPromise', fetchAndSave('fetchNavigatorMetadata', 'navigatorMeta', dataCatalogEntry, apiOptions));
+        return dataCatalogEntry.trackedPromise('navigatorMetaPromise', fetchAndSave('fetchNavigatorMetadata', 'navigatorMeta', dataCatalogEntry, apiOptions)).done(function (navigatorMeta) {
+          if (navigatorMeta && dataCatalogEntry.commentObservable) {
+            dataCatalogEntry.commentObservable(dataCatalogEntry.getResolvedComment());
+          }
+        });
       }
       dataCatalogEntry.navigatorMetaPromise = $.Deferred().reject();
       return dataCatalogEntry.navigatorMetaPromise;
@@ -569,6 +573,16 @@ var DataCatalog = (function () {
 
       self.definition = definition;
 
+      if (!self.definition) {
+        if (self.path.length === 0) {
+          self.definition = { type: 'source' }
+        } else if (self.path.length === 1) {
+          self.definition = { type: 'database' }
+        } else if (self.path.length === 2) {
+          self.definition = { type: 'table' }
+        }
+      }
+
       self.reset();
     }
 
@@ -851,6 +865,9 @@ var DataCatalog = (function () {
               if (matchingChildEntry) {
                 matchingChildEntry.navigatorMeta = entity;
                 matchingChildEntry.navigatorMetaPromise = $.Deferred().resolve(matchingChildEntry.navigatorMeta).promise();
+                if (entity && matchingChildEntry.commentObservable) {
+                  matchingChildEntry.commentObservable(matchingChildEntry.getResolvedComment());
+                }
                 matchingChildEntry.saveLater();
               }
             });
@@ -1000,6 +1017,20 @@ var DataCatalog = (function () {
       return self.sourceMeta && self.sourceMeta.comment || '';
     };
 
+    /**
+     * This can be used to get an observable for the comment which will be updated once a comment has been
+     * resolved.
+     *
+     * @return {ko.observable}
+     */
+    DataCatalogEntry.prototype.getCommentObservable = function () {
+      var self = this;
+      if (!self.commentObservable) {
+        self.commentObservable = ko.observable(self.getResolvedComment());
+      }
+      return self.commentObservable;
+    };
+
     /**
      * Checks whether the comment is known and has been loaded from the proper source
      *
@@ -1420,17 +1451,31 @@ var DataCatalog = (function () {
      * Returns the type of the entry. It will be accurate once the source meta has been loaded or if loaded from
      * a parent entry via getChildren().
      *
-     * For complex entries the type definition is stripped to either 'array', 'map' or 'struct'
+     * The returned string is always lower case and for complex entries the type definition is stripped to
+     * either 'array', 'map' or 'struct'.
      *
      * @return {string}
      */
     DataCatalogEntry.prototype.getType = function () {
       var self = this;
-      var type = self.sourceMeta && self.sourceMeta.type || self.definition.type || '';
+      var type = self.getRawType();
       if (type.indexOf('<') !== -1) {
         type = type.substring(0, type.indexOf('<'));
       }
-      return type;
+      return type.toLowerCase();
+    };
+
+    /**
+     * Returns the raw type of the entry. It will be accurate once the source meta has been loaded or if loaded from
+     * a parent entry via getChildren().
+     *
+     * For complex entries the type definition is the full version.
+     *
+     * @return {string}
+     */
+    DataCatalogEntry.prototype.getRawType = function () {
+      var self = this;
+      return self.sourceMeta && self.sourceMeta.type || self.definition.type || '';
     };
 
     /**

+ 17 - 1
desktop/core/src/desktop/static/desktop/js/ko.hue-bindings.js

@@ -4810,7 +4810,23 @@
                 bottom: endCoordinates.pageY + editor.renderer.lineHeight
               };
 
-              if (token.parseLocation && !token.notFound) {
+              if (token.parseLocation && token.parseLocation.identifierChain && !token.notFound) {
+                // Database, table and field
+                var path = $.map(token.parseLocation.identifierChain, function (identifier) {
+                  return identifier.name;
+                });
+                DataCatalog.getEntry({sourceType: snippet.type(), path: path}).done(function (entry) {
+                  huePubSub.publish('context.popover.show', {
+                    data: {
+                      type: 'catalogEntry',
+                      catalogEntry: entry
+                    },
+                    pinEnabled: true,
+                    source: source
+                  });
+                });
+              } else if (token.parseLocation && !token.notFound) {
+                // Asterisk, function etc.
                 huePubSub.publish('context.popover.show', {
                   data: token.parseLocation,
                   sourceType: snippet.type(),

+ 461 - 0
desktop/core/src/desktop/static/desktop/less/components/hue-popover.less

@@ -0,0 +1,461 @@
+/*
+ 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.
+*/
+
+.hue-popover {
+  .display-flex;
+  position: absolute;
+  .flex-direction(column);
+  top: 0;
+  left: 0;
+  z-index: 1039;
+  padding: 1px;
+  background-color: @cui-white;
+  -webkit-background-clip: padding-box;
+  background-clip: padding-box;
+  border: 1px solid transparent;
+}
+
+.hue-context-popover {
+  padding: 0;
+  border: none;
+}
+
+.hue-popover.hue-popover-top {
+  margin-top: -5px;
+}
+
+.hue-popover.hue-popover-top .hue-popover-arrow {
+  bottom: -6px;
+  left: 50%;
+  margin-left: -6px;
+  border-top-color: @hue-primary-color-dark;
+  border-bottom-width: 0;
+}
+
+.hue-popover.hue-popover-top .hue-popover-arrow::after {
+  bottom: 1px;
+  margin-left: -3px;
+  content: "";
+  border-top-color: @hue-primary-color-dark;
+  border-bottom-width: 0;
+}
+
+.hue-popover.hue-popover-right {
+  margin-left: 5px;
+}
+
+.hue-popover.hue-popover-right .hue-popover-arrow {
+  top: 50%;
+  left: -6px;
+  margin-top: -6px;
+  border-right-color: @hue-primary-color-dark;
+  border-left-width: 0;
+}
+
+.hue-popover.hue-popover-right .hue-popover-arrow::after {
+  bottom: -3px;
+  left: 1px;
+  content: "";
+  border-right-color: @hue-primary-color-dark;
+  border-left-width: 0;
+}
+
+.hue-popover.hue-popover-bottom {
+  margin-top: 7px;
+}
+
+.hue-popover.hue-popover-bottom .hue-popover-arrow {
+  top: -6px;
+  left: 50%;
+  margin-left: -6px;
+  border-top-width: 0;
+  border-bottom-color: @hue-primary-color-dark;
+}
+
+.hue-popover.hue-popover-bottom .hue-popover-arrow::after {
+  top: 3px;
+  margin-left: -3px;
+  content: "";
+  border-top-width: 0;
+  border-bottom-color: @hue-primary-color-dark;
+}
+
+.hue-popover.hue-popover-left {
+  margin-left: -5px;
+}
+
+.hue-popover.hue-popover-left .hue-popover-arrow {
+  top: 50%;
+  right: -6px;
+  margin-top: -3px;
+  border-right-width: 0;
+  border-left-color: @hue-primary-color-dark;
+}
+
+.hue-popover.hue-popover-left .hue-popover-arrow::after {
+  right: 2px;
+  bottom: -3px;
+  content: "";
+  border-right-width: 0;
+  border-left-color: @hue-primary-color-dark;
+}
+
+.hue-popover-title {
+  .flex(0 1 auto);
+  .nowrap-ellipsis;
+  overflow: hidden;
+  padding: 6px 10px;
+  margin: 0 30px 0 0;
+  font-size: 14px;
+  background-color: @cui-white;
+
+  .hue-popover-title-icon {
+    margin-top: 3px;
+  }
+
+  .hue-popover-title-text {
+    padding-left: 4px;
+  }
+
+  .hue-popover-title-actions {
+    position: absolute;
+    right: 6px;
+    top: 8px;
+  }
+}
+
+.hue-popover-title:empty {
+  display: none;
+}
+
+.hue-popover-arrow, .hue-popover-arrow::after {
+  position: absolute;
+  display: block;
+  width: 0;
+  height: 0;
+  border-color: transparent;
+  border-style: solid;
+}
+
+.hue-popover-arrow {
+  border-width: 6px;
+}
+
+.hue-popover-top {
+  border-radius: @hue-panel-border-radius;
+  background-color: @cui-white;
+  .hue-box-shadow-bottom;
+}
+
+.hue-popover-right {
+  border-radius: @hue-panel-border-radius;
+  background-color: @cui-white;
+  .hue-box-shadow-right;
+}
+
+.hue-popover-left {
+  border-radius: @hue-panel-border-radius;
+  background-color: @cui-white;
+  .hue-box-shadow-left;
+}
+
+.context-popover-content {
+  padding: 5px;
+  .display-flex();
+  .flex(1 1 100%);
+  .flex-direction(column);
+  padding: 0;
+  overflow: hidden;
+}
+
+.context-popover-tab-pane {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  width: 100%;
+}
+
+.context-popover-tabs {
+  .flex(0 1 auto);
+  border-bottom: 1px solid @cui-default-border-color;
+  margin-left: -1px;
+  margin-right: -1px;
+  padding-left: 15px;
+}
+
+.context-popover-tab-container {
+  position: relative;
+  .flex(1 1 100%);
+  border: none;
+  overflow: auto;
+}
+
+.context-popover-tab {
+  padding-top: 0 !important;
+  padding-bottom: 5px !important;
+  margin-bottom: -1px !important
+}
+
+.context-popover-flex {
+  .display-flex();
+  .flex-flow(column nowrap);
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  width: 100%;
+}
+
+.context-popover-flex-header {
+  .flex(0 0 35px);
+}
+
+.context-popover-flex-fill {
+  overflow-y: auto;
+  overflow-x: hidden;
+  position: relative;
+  margin: 0 10px;
+  .flex(1 1 100%);
+
+  .alert {
+    margin: 0 5px;
+  }
+}
+
+.context-popover-table-details {
+  .flex(1);
+  padding: 10px 10px 0 10px;
+}
+
+.context-popover-header {
+  font-size: 15px;
+  margin: 10px;
+}
+
+.context-popover-section {
+  margin-left: 15px;
+}
+.context-popover-doc-header-link {
+  .flex(1);
+  margin-top: 10px;
+  font-size: 18px;
+}
+
+.context-popover-doc-description {
+  .flex(1);
+  margin-top: 5px;
+
+  > em {
+    font-style: normal;
+    font-weight: bold;
+  }
+}
+
+.context-popover-doc-contents {
+  .flex(1 1 100%);
+  margin-top: 8px;
+
+  .ace_editor,
+  .ace_layer {
+    overflow: visible !important;
+  }
+}
+
+.context-popover-flex-bottom-links {
+  .flex(0 0 35px);
+  border-top: 1px solid @cui-default-border-color;
+  z-index: 100;
+  background-color: @cui-white;
+}
+
+.context-popover-link-row {
+  float: right;
+  margin: 8px 15px 0 10px;
+}
+
+.context-popover-link-row a {
+  margin-left: 10px;
+}
+
+.context-popover-inline-search {
+  border-radius: 8px !important;
+  min-height: 18px !important;
+  height: 18px !important;
+  margin: 0 5px 0 5px !important;
+  padding-right: 18px !important;
+}
+
+.context-popover-inline-autocomplete {
+  height: 26px;
+  margin: 0 0 10px 0;
+
+  .inline-autocomplete-container {
+    height: 26px;
+
+    > div {
+      right: 10px !important;
+    }
+
+    > div input {
+      padding: 3px 5px;
+      font-size: 14px !important;
+      line-height: 14px !important;
+    }
+  }
+}
+
+.context-popover-empty-columns {
+  letter-spacing: 0.035em;
+  margin-top: 50px;
+  font-size: 14px;
+  color: @cui-gray-700;
+  text-align: center;
+}
+
+.context-popover-sample {
+  text-align: left;
+  padding: 3px;
+  overflow: hidden;
+  height: 100%;
+
+  th {
+    border-right: 1px solid @cui-gray-200;
+  }
+
+  td {
+    border-right: 1px solid @cui-gray-200;
+    white-space: nowrap;
+  }
+
+  .fixed-first-column {
+    margin-left: 0 !important;
+    table>tbody>tr>td:first-child {
+      background-color: @cui-gray-050;
+    }
+  }
+
+  .fixed-header-row {
+    border-bottom: 1px solid @cui-gray-200;
+  }
+
+  .fixed-first-cell {
+    border-right: 1px solid @cui-gray-200;
+
+    thead th {
+      height: 23px;
+      border-bottom: 1px solid @cui-gray-200 !important;
+    }
+  }
+}
+
+.context-popover-flex-fill.context-popover-sample-container {
+  margin: 0;
+}
+
+#contextPopover .hue-popover {
+  position: fixed;
+}
+
+.context-popover-comment {
+  .flex(1);
+  margin-top: 2px;
+  margin-bottom: 8px;
+  padding-left: 4px;
+}
+
+.context-popover-sql {
+  .flex(1 1 100%);
+}
+
+.context-popover-attributes {
+  .flex(1);
+  margin-bottom: 10px;
+  margin-left: 4px;
+  font-size: 13px;
+
+  .context-popover-attribute {
+    display: inline-block;
+    //background-color: @cui-gray-100;
+    border-radius: 4px;
+    padding: 2px 4px 2px 0;
+
+    white-space: pre;
+
+    &:not(:last-child) {
+      margin-right: 10px;
+    }
+
+    div:first-child {
+      display: inline-block;
+      color: @cui-gray-700;
+      margin-right: 3px;
+    }
+
+    div:last-child {
+      display: inline-block;
+      background-color: #FFF;
+      //color: @hue-primary-color-dark;
+    }
+  }
+}
+
+.catalog-entries-list-container {
+  .flex(1 1 100%);
+
+  table td.sample-column {
+    max-width: 500px;
+    .nowrap-ellipsis();
+  }
+}
+
+.context-popover-inner-content {
+  position: absolute;
+  .display-flex();
+  top: 0;
+  bottom: 0;
+  right: 0;
+  left: 0;
+  overflow: hidden;
+  .flex-direction(column);
+
+  .hue-tags {
+    margin-top: 2px;
+  }
+}
+
+.context-popover-entries-list {
+  margin: 10px 4px 4px 4px;
+}
+
+.context-popover-title {
+  .display-flex;
+  .flex-direction(row);
+  width: 100%;
+  padding:0;
+  background-color: @cui-gray-050;
+}
+
+.context-popover-breadcrumbs {
+  .flex(1 1 100%);
+  .nowrap-ellipsis();
+  padding: 5px;
+}
+
+.context-popover-close {
+  .flex(1 0 15px);
+  padding: 5px;
+}

+ 9 - 3
desktop/core/src/desktop/static/desktop/less/components/hue-selectize.less

@@ -41,9 +41,15 @@
   margin-right: 4px;
 }
 
-.selectize-input {
-  padding-top: 6px !important;
-  padding-bottom: 5px !important;
+.hue-tags .selectize-input {
+  padding-top: 6px;
+  padding-bottom: 5px;
+}
+
+.hue-tags.read-only-tags {
+  .selectize-input {
+    padding: 2px !important;
+  }
 }
 
 .selectize-dropdown {

+ 0 - 180
desktop/core/src/desktop/static/desktop/less/hue-assist.less

@@ -667,186 +667,6 @@
   background: url(/static/desktop/art/spinner-input.gif) no-repeat right -8px center !important;
 }
 
-// --- Context Popover ---
-
-.context-popover-content {
-  .display-flex();
-  .flex(1 1 100%);
-  .flex-direction(column);
-  padding: 0;
-  overflow: hidden;
-}
-
-.context-popover-tab-pane {
-  position: absolute;
-  top: 0;
-  bottom: 0;
-  width: 100%;
-}
-
-.context-popover-tabs {
-  .flex(0 1 auto);
-  border-bottom: 1px solid @cui-default-border-color;
-  margin-left: -1px;
-  margin-right: -1px;
-  padding-left: 15px;
-}
-
-.context-popover-tab-container {
-  position: relative;
-  .flex(1 1 100%);
-  border: none;
-  overflow: auto;
-}
-
-.context-popover-tab {
-  padding-top: 0 !important;
-  padding-bottom: 5px !important;
-  margin-bottom: -1px !important
-}
-
-.context-popover-flex {
-  .display-flex();
-  .flex-flow(column nowrap);
-  position: absolute;
-  top: 0;
-  bottom: 0;
-  width: 100%;
-}
-
-.context-popover-flex-header {
-  .flex(0 0 35px);
-}
-
-.context-popover-flex-fill {
-  overflow: hidden;
-  position: relative;
-  margin: 0 10px;
-  .flex(1 1 100%);
-
-  .alert {
-    margin: 0 5px;
-  }
-}
-
-.context-popover-table-details {
-  .flex(1);
-  padding: 10px 10px 0 10px;
-}
-
-.context-popover-header {
-  font-size: 15px;
-  margin: 10px;
-}
-
-.context-popover-section {
-  margin-left: 15px;
-}
-.context-popover-doc-header-link {
-  font-size: 18px;
-}
-
-.context-popover-doc-description {
-  margin-top: 5px;
-
-  > em {
-    font-style: normal;
-    font-weight: bold;
-  }
-}
-
-.context-popover-flex-bottom-links {
-  .flex(0 0 35px);
-  border-top: 1px solid @cui-default-border-color;
-  z-index: 100;
-  background-color: @cui-white;
-}
-
-.context-popover-link-row {
-  float: right;
-  margin: 8px 15px 0 10px;
-}
-
-.context-popover-link-row a {
-  margin-left: 10px;
-}
-
-.context-popover-inline-search {
-  border-radius: 8px !important;
-  min-height: 18px !important;
-  height: 18px !important;
-  margin: 0 5px 0 5px !important;
-  padding-right: 18px !important;
-}
-
-.context-popover-inline-autocomplete {
-  height: 26px;
-  margin: 0 10px 10px 10px;
-
-  .inline-autocomplete-container {
-    height: 26px;
-
-    > div {
-      right: 10px !important;
-    }
-
-    > div input {
-      padding: 3px 5px;
-      font-size: 14px !important;
-      line-height: 14px !important;
-    }
-  }
-
-}
-
-.context-popover-empty-columns {
-  letter-spacing: 0.035em;
-  margin-top: 50px;
-  font-size: 14px;
-  color: @cui-gray-700;
-  text-align: center;
-}
-
-.context-popover-sample {
-  text-align: left;
-  padding: 3px;
-  overflow: hidden;
-  height: 100%;
-
-  th {
-    border-right: 1px solid @cui-gray-200;
-  }
-
-  td {
-    border-right: 1px solid @cui-gray-200;
-    white-space: nowrap;
-  }
-
-  .fixed-first-column {
-    margin-left: 0 !important;
-    table>tbody>tr>td:first-child {
-      background-color: @cui-gray-050;
-    }
-  }
-
-  .fixed-header-row {
-    border-bottom: 1px solid @cui-gray-200;
-  }
-
-  .fixed-first-cell {
-    border-right: 1px solid @cui-gray-200;
-
-    thead th {
-      height: 23px;
-      border-bottom: 1px solid @cui-gray-200 !important;
-    }
-  }
-}
-
-.context-popover-flex-fill.context-popover-sample-container {
-  margin: 0;
-}
-
 .assist-panel-close {
   position: absolute;
   right: 5px;

+ 1 - 0
desktop/core/src/desktop/static/desktop/less/hue-cross-version.less

@@ -24,6 +24,7 @@
 @import "components/hue-gridster.less";
 @import "components/hue-inputautocomplete.less";
 @import "components/hue-notify.less";
+@import "components/hue-popover.less";
 @import "components/hue-qq-upload.less";
 @import "components/hue-query-builder.less";
 @import "components/hue-ribbon.less";

+ 20 - 174
desktop/core/src/desktop/static/desktop/less/hue4.less

@@ -292,7 +292,7 @@
   position: relative;
 }
 
-.search-container-top .inline-autocomplete-container {
+.search-container-top > .inline-autocomplete-container {
   position: relative;
   margin-top: 8px;
   height: 30px;
@@ -465,148 +465,6 @@ a:visited {
   }
 }
 
-.hue-popover {
-  .display-flex;
-  position: absolute;
-  .flex-direction(column);
-  top: 0;
-  left: 0;
-  z-index: 1039;
-  padding: 1px;
-  background-color: @cui-white;
-  -webkit-background-clip: padding-box;
-  background-clip: padding-box;
-  border: 1px solid transparent;
-}
-
-.hue-context-popover {
-  padding: 0;
-  border: none;
-}
-
-.hue-popover.hue-popover-top {
-  margin-top: -5px;
-}
-
-.hue-popover.hue-popover-top .hue-popover-arrow {
-  bottom: -6px;
-  left: 50%;
-  margin-left: -6px;
-  border-top-color: @hue-primary-color-dark;
-  border-bottom-width: 0;
-}
-
-.hue-popover.hue-popover-top .hue-popover-arrow::after {
-  bottom: 1px;
-  margin-left: -3px;
-  content: "";
-  border-top-color: @hue-primary-color-dark;
-  border-bottom-width: 0;
-}
-
-.hue-popover.hue-popover-right {
-  margin-left: 5px;
-}
-
-.hue-popover.hue-popover-right .hue-popover-arrow {
-  top: 50%;
-  left: -6px;
-  margin-top: -6px;
-  border-right-color: @hue-primary-color-dark;
-  border-left-width: 0;
-}
-
-.hue-popover.hue-popover-right .hue-popover-arrow::after {
-  bottom: -3px;
-  left: 1px;
-  content: "";
-  border-right-color: @hue-primary-color-dark;
-  border-left-width: 0;
-}
-
-.hue-popover.hue-popover-bottom {
-  margin-top: 7px;
-}
-
-.hue-popover.hue-popover-bottom .hue-popover-arrow {
-  top: -6px;
-  left: 50%;
-  margin-left: -6px;
-  border-top-width: 0;
-  border-bottom-color: @hue-primary-color-dark;
-}
-
-.hue-popover.hue-popover-bottom .hue-popover-arrow::after {
-  top: 3px;
-  margin-left: -3px;
-  content: "";
-  border-top-width: 0;
-  border-bottom-color: @hue-primary-color-dark;
-}
-
-.hue-popover.hue-popover-left {
-  margin-left: -5px;
-}
-
-.hue-popover.hue-popover-left .hue-popover-arrow {
-  top: 50%;
-  right: -6px;
-  margin-top: -3px;
-  border-right-width: 0;
-  border-left-color: @hue-primary-color-dark;
-}
-
-.hue-popover.hue-popover-left .hue-popover-arrow::after {
-  right: 2px;
-  bottom: -3px;
-  content: "";
-  border-right-width: 0;
-  border-left-color: @hue-primary-color-dark;
-}
-
-.hue-popover-title {
-  .flex(0 1 auto);
-  padding: 6px 10px;
-  margin: 0;
-  font-size: 14px;
-  background-color: @cui-white;
-}
-
-.hue-popover-title:empty {
-  display: none;
-}
-
-.hue-popover-arrow, .hue-popover-arrow::after {
-  position: absolute;
-  display: block;
-  width: 0;
-  height: 0;
-  border-color: transparent;
-  border-style: solid;
-}
-
-.hue-popover-arrow {
-  border-width: 6px;
-}
-
-.hue-popover-top {
-  border-radius: @hue-panel-border-radius;
-  background-color: @cui-white;
-  .hue-box-shadow-bottom;
-}
-
-.hue-popover-right {
-  border-radius: @hue-panel-border-radius;
-  background-color: @cui-white;
-  .hue-box-shadow-right;
-}
-
-.hue-popover-left {
-  border-radius: @hue-panel-border-radius;
-  background-color: @cui-white;
-  .hue-box-shadow-left;
-}
-
 .hue-inner-drop-down {
   margin: 0 !important;
   padding: 0;
@@ -717,33 +575,6 @@ a:visited {
   background-color: @cui-gray-200;
 }
 
-#contextPopover .hue-popover {
-  position: fixed;
-}
-
-.context-popover-title {
-  .display-flex;
-  .flex-direction(row);
-  width: 100%;
-  padding:0;
-  background-color: @cui-gray-050;
-}
-
-.context-popover-breadcrumbs {
-  .flex(1 1 100%);
-  .nowrap-ellipsis();
-  padding: 5px;
-}
-
-.context-popover-close {
-  .flex(1 0 15px);
-  padding: 5px;
-}
-
-.context-popover-content {
-  padding: 5px;
-}
-
 .hue-breadcrumb-container {
   .hue-breadcrumb {
     display: inline-block;
@@ -940,7 +771,7 @@ a:visited {
   top: 35px;
   left: 0;
   width: 100%;
-  height: 400px;
+  height: 500px;
   .hue-box-shadow-bottom();
 
   .global-search-alternatives {
@@ -997,19 +828,34 @@ a:visited {
       }
     }
   }
+
   .global-search-preview {
-    padding-top: 10px;
     .display-flex();
     .flex(1);
 
     .global-search-close-preview {
       position: absolute;
-      top: 3px;
-      right: 10px;
+      z-index: 100;
+      top: 8px;
+      right: 7px;
       width: 18px;
       height: 18px;
     }
 
+    .context-popover-content {
+      margin-top: 10px;
+    }
+
+    .global-search-catalog-entry {
+      .display-flex();
+      .flex-direction(column);
+      width: 100%;
+
+      .hue-popover-title-actions {
+        right: 29px !important;
+      }
+    }
+
     .context-popover-tab {
       background-color: transparent !important;
     }

+ 2 - 0
desktop/core/src/desktop/templates/ko_components.mako

@@ -15,6 +15,7 @@
 ## limitations under the License.
 
 <%namespace name="koBreadCrumbs" file="/ko_components/ko_breadcrumbs.mako" />
+<%namespace name="koCatalogEntriesTable" file="/ko_components/ko_catalog_entries_table.mako" />
 <%namespace name="koContextPopover" file="/ko_components/ko_context_popover.mako" />
 <%namespace name="koDropDown" file="/ko_components/ko_drop_down.mako" />
 <%namespace name="koFavoriteApp" file="/ko_components/ko_favorite_app.mako" />
@@ -28,6 +29,7 @@
 
 <%def name="all()">
   ${ koBreadCrumbs.breadCrumbs() }
+  ${ koCatalogEntriesTable.catalogEntriesTable() }
   ${ koContextPopover.contextPopover() }
   ${ koDropDown.dropDown() }
   ${ koFavoriteApp.favoriteApp() }

+ 333 - 0
desktop/core/src/desktop/templates/ko_components/ko_catalog_entries_table.mako

@@ -0,0 +1,333 @@
+## 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.
+
+<%!
+from django.utils.translation import ugettext as _
+from desktop.lib.i18n import smart_unicode
+from desktop.views import _ko
+%>
+
+<%def name="catalogEntriesTable()">
+
+  <script type="text/html" id="catalog-entries-list-template">
+    <!-- ko if: !loading() -->
+    <div class="catalog-entries-list-filter context-popover-inline-autocomplete">
+      <!-- ko component: {
+        name: 'inline-autocomplete',
+        params: {
+          querySpec: querySpec,
+          facets: facets,
+          knownFacetValues: knownFacetValues,
+          autocompleteFromEntries: autocompleteFromEntries
+        }
+      } --><!-- /ko -->
+    </div>
+    <!-- /ko -->
+
+    <div class="catalog-entries-list-container" data-bind="niceScroll" style="overflow:hidden;">
+      <!-- ko hueSpinner: { spin: loading, center: true, size: 'xlarge' } --><!-- /ko -->
+
+      <!-- ko if: !loading() && catalogEntry.isDatabase() -->
+      <table class="table table-condensed table-nowrap">
+        <thead>
+        <tr>
+          <th data-bind="text: catalogEntry.getSourceType() !== 'solr' ? '${ _ko("Table") }' : '${ _ko("Collection") }'"></th>
+          <th>${ _("Description") } <!-- ko if: loadingNav --><i class="fa fa-spinner fa-spin"></i><!-- /ko --></th>
+        </tr>
+        </thead>
+        <!-- ko if: filteredEntries().length -->
+        <tbody data-bind="foreach: filteredEntries">
+        <tr>
+          <td><a href="javascript: void(0);" data-bind="text: catalogEntry.name, click: onClick, attr: { 'title': catalogEntry.getTitle() }"></a></td>
+          <td data-bind="text: catalogEntry.getCommentObservable(), attr: { 'title': catalogEntry.getCommentObservable() }"></td>
+        </tr>
+        </tbody>
+        <!-- /ko -->
+        <!-- ko if: filteredEntries().length === 0 -->
+        <tbody>
+        <tr>
+          <!-- ko ifnot: hasErrors -->
+          <td colspan="2" style="font-style: italic;">${ _("No entries found") }</td>
+          <!-- /ko -->
+          <!-- ko if: hasErrors -->
+          <td colspan="2" style="font-style: italic;">${ _("Error loading entries") }</td>
+          <!-- /ko -->
+        </tr>
+        </tbody>
+        <!-- /ko -->
+      </table>
+      <!-- /ko -->
+
+      <!-- ko if: !loading() && (catalogEntry.isTableOrView() || catalogEntry.isComplex()) -->
+      <table class="table table-condensed table-nowrap">
+        <thead>
+        <tr>
+          <th><span data-bind="text: catalogEntry.getSourceType() !== 'solr' ? '${ _ko("Column") }' : '${ _ko("Field") }'"></span> (<span data-bind="text: filteredEntries().length"></span>)</th>
+          <th>${ _("Type") }</th>
+          <th>${ _("Description") } <!-- ko if: loadingNav --><i class="fa fa-spinner fa-spin"></i><!-- /ko --></th>
+          <th colspan="2">${ _("Sample") } <!-- ko if: loadingSamples --><i class="fa fa-spinner fa-spin"></i><!-- /ko --></th>
+        </tr>
+        </thead>
+        <!-- ko if: filteredEntries().length -->
+        <tbody data-bind="foreach: filteredEntries">
+        <tr>
+          <td width="10%" data-bind="attr: { 'title': catalogEntry.name + ' - ${ _ko("Click for more details") }' }">
+            <a href="javascript: void(0);" data-bind="click: onClick">
+              <span data-bind="text: catalogEntry.name"></span>
+              <!-- ko if: catalogEntry.isPrimaryKey() -->
+              &nbsp;<i title="${ _("Primary Key") }"class="fa fa-key"></i>
+              <!-- /ko -->
+            </a>
+          </td>
+          <td width="10%" data-bind="text: catalogEntry.getType(), attr: { 'title': catalogEntry.getRawType() }"></td>
+          <td width="30%" data-bind="text: catalogEntry.getCommentObservable(), attr: { 'title': catalogEntry.getCommentObservable() }"></td>
+          <td width="25%" class="sample-column" data-bind="html: firstSample, attr: { 'title': hueUtils.html2text(firstSample()) }"></td>
+          <td width="25%" class="sample-column" data-bind="html: secondSample, attr: { 'title': hueUtils.html2text(secondSample()) }"></td>
+        </tr>
+        </tbody>
+        <!-- /ko -->
+        <!-- ko if: filteredEntries().length === 0 -->
+        <tbody>
+        <tr>
+          <!-- ko ifnot: hasErrors -->
+          <td colspan="5" style="font-style: italic;">${ _("No entries found") }</td>
+          <!-- /ko -->
+          <!-- ko if: hasErrors -->
+          <td colspan="5" style="font-style: italic;">${ _("Error loading entries") }</td>
+          <!-- /ko -->
+        </tr>
+        </tbody>
+        <!-- /ko -->
+      </table>
+      <!-- /ko -->
+
+      <!-- ko if: !loading() && catalogEntry.isField() && !catalogEntry.isComplex() -->
+      <table class="table table-condensed table-nowrap">
+        <thead>
+          <tr>
+            <th>${ _("Sample") }</th>
+          </tr>
+        </thead>
+        <tbody data-bind="foreach: filteredColumnSamples">
+          <tr>
+            <td data-bind="html: $data, attr: { 'title': $data }"></td>
+          </tr>
+        </tbody>
+        <!-- ko if: filteredColumnSamples().length === 0 -->
+        <tbody>
+        <tr>
+          <!-- ko ifnot: hasErrors -->
+          <td style="font-style: italic;">${ _("No entries found") }</td>
+          <!-- /ko -->
+          <!-- ko if: hasErrors -->
+          <td style="font-style: italic;">${ _("Error loading entries") }</td>
+          <!-- /ko -->
+        </tr>
+        </tbody>
+        <!-- /ko -->
+      </table>
+      <!-- /ko -->
+    </div>
+  </script>
+
+  <script type="text/javascript">
+    (function () {
+
+      function SampleEnrichedEntry(catalogEntry, onClick) {
+        var self = this;
+        self.catalogEntry = catalogEntry;
+        self.firstSample = ko.observable();
+        self.secondSample = ko.observable();
+        self.onClick = onClick;
+      }
+
+      function CatalogEntriesList(params) {
+        var self = this;
+        self.catalogEntry = params.catalogEntry;
+        self.entries = ko.observableArray();
+
+        // If the entry is a column without children
+        self.columnSamples = ko.observableArray();
+        self.querySpec = ko.observable();
+        self.cancellablePromises = [];
+        self.loading = ko.observable(false);
+        self.loadingNav = ko.observable(false);
+        self.loadingSamples = ko.observable(false);
+        self.hasErrors = ko.observable(false);
+
+        // TODO: Can be computed based on contents (instead of always suggesting all col types etc.)
+        self.knownFacetValues = ko.pureComputed(function () {
+          if (self.catalogEntry.isDatabase()) {
+            return { 'type': { 'table': -1, 'view': -1 } };
+          } else if (self.catalogEntry.isTableOrView()) {
+            var typeIndex = { 'type': {} };
+
+            // Issue with filteredEntries is that it's not updated in time when typing,
+            // i.e. type:| doesn't automatically open the suggestion list.
+            self.entries().forEach(function (entry) {
+              var type = entry.catalogEntry.getType().toLowerCase();
+              if (!typeIndex['type'][type]) {
+                typeIndex['type'][type] = 1;
+              } else {
+                typeIndex['type'][type]++;
+              }
+            });
+            return typeIndex;
+          }
+        });
+
+        self.facets = self.catalogEntry.isField() && !self.catalogEntry.isComplex() ? [] : ['type'];
+
+        self.filteredColumnSamples = ko.pureComputed(function () {
+          if (!self.querySpec() || self.querySpec().query === '') {
+            return self.columnSamples();
+          }
+
+          return self.columnSamples().filter(function (sampleValue) {
+            if (typeof sampleValue === 'undefined' || sampleValue === null) {
+              return false;
+            }
+            return self.querySpec().text.every(function (text) {
+              var textLower = text.toLowerCase();
+              return sampleValue.toString().toLowerCase().indexOf(textLower) !== -1;
+            });
+          });
+        });
+
+        self.filteredEntries = ko.pureComputed(function () {
+          if (!self.querySpec() || self.querySpec().query === '') {
+            return self.entries();
+          }
+
+          var facets = self.querySpec().facets;
+          var isFacetMatch = !facets || Object.keys(facets).length === 0 || !facets['type']; // So far only type facet is used for SQL
+          var isTextMatch = !self.querySpec().text || self.querySpec().text.length === 0;
+
+          return self.entries().filter(function (entry) {
+            var match = true;
+
+            if (!isFacetMatch) {
+              if (entry.catalogEntry.isField()) {
+                match = !!facets['type'][entry.catalogEntry.getType().toLowerCase()];
+              } else if (entry.catalogEntry.isTableOrView()) {
+                match = (facets['type']['table'] && entry.catalogEntry.isTable()) || (facets['type']['view'] && entry.catalogEntry.isView()) ;
+              }
+            }
+
+            if (match && !isTextMatch) {
+              match = self.querySpec().text.every(function (text) {
+                var textLower = text.toLowerCase();
+                return entry.catalogEntry.name.toLowerCase().indexOf(textLower) !== -1
+                  || entry.catalogEntry.getResolvedComment().toLowerCase().indexOf(textLower) !== -1;
+              });
+            }
+
+            return match;
+          });
+        });
+
+        self.autocompleteFromEntries = function (nonPartial, partial) {
+          var result = [];
+          var partialLower = partial.toLowerCase();
+          self.entries().forEach(function (entry) {
+            if (entry.catalogEntry.name.toLowerCase().indexOf(partialLower) === 0) {
+              result.push(nonPartial + partial + entry.catalogEntry.name.substring(partial.length))
+            }
+          });
+          return result;
+        };
+
+        if (self.catalogEntry.isField() && !self.catalogEntry.isComplex()) {
+          self.loading(true);
+          self.cancellablePromises.push(self.catalogEntry.getSample({ silenceErrors: true, cancellable: true }).done(function (samples) {
+            if (samples.data && samples.data.length) {
+              self.columnSamples(samples.data);
+            }
+          }).fail(function () {
+            self.hasErrors(true);
+          }).always(function () {
+            self.loading(false);
+          }));
+        } else {
+          self.loading(true);
+          var onClick = function (sampleEnrichedEntry) {
+            params.onClick(sampleEnrichedEntry.catalogEntry);
+          };
+          var childPromise = self.catalogEntry.getChildren({ silenceErrors: true, cancellable: true }).done(function (childEntries) {
+            self.entries($.map(childEntries, function (entry) { return new SampleEnrichedEntry(entry, onClick) }));
+          }).fail(function () {
+            self.hasErrors(true);
+          }).always(function () {
+            self.loading(false);
+          });
+
+          var navMetaPromise = self.catalogEntry.loadNavigatorMetaForChildren({ silenceErrors: true, cancellable: true }).always(function () {
+            self.loadingNav(false);
+          });
+
+          self.cancellablePromises.push(navMetaPromise);
+          self.cancellablePromises.push(childPromise);
+
+          if (self.catalogEntry.isTableOrView() || self.catalogEntry.isField()) {
+            self.loadingSamples(true);
+            self.cancellablePromises.push(self.catalogEntry.getSample({ silenceErrors: true, cancellable: true }).done(function (sample) {
+              childPromise.done(function () {
+                if (sample.meta && sample.meta.length && sample.data && sample.data.length) {
+                  var entryIndex = {};
+                  self.entries().forEach(function (entry) {
+                    entryIndex[entry.catalogEntry.name] = entry;
+                  });
+                  for (var i = 0; i < sample.meta.length; i++) {
+                    var name = sample.meta[i].name;
+                    if (name.toLowerCase().indexOf(self.catalogEntry.name.toLowerCase() + '.') === 0) {
+                      name = name.substring(self.catalogEntry.name.length + 1);
+                    }
+                    var sampleEntry = entryIndex[name];
+                    if (sampleEntry) {
+                      sampleEntry.firstSample(sample.data[0][i]);
+                      if (sample.data.length > 1) {
+                        sampleEntry.secondSample(sample.data[1][i])
+                      }
+                    }
+                  }
+                }
+              }).always(function () {
+                self.loadingSamples(false);
+              })
+            }).fail(function () {
+              self.loadingSamples(false);
+            }));
+          }
+        }
+      }
+
+      CatalogEntriesList.prototype.dispose = function () {
+        var self = this;
+        while (self.cancellablePromises.length) {
+          var promise = self.cancellablePromises.pop();
+          if (promise.cancel) {
+            promise.cancel();
+          }
+        }
+      };
+
+      ko.components.register('catalog-entries-list', {
+        viewModel: CatalogEntriesList,
+        template: { element: 'catalog-entries-list-template' }
+      });
+    })();
+  </script>
+</%def>

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 228 - 670
desktop/core/src/desktop/templates/ko_components/ko_context_popover.mako


+ 1 - 1
desktop/core/src/desktop/templates/ko_components/ko_global_search.mako

@@ -81,7 +81,7 @@ from desktop.views import _ko
       </div>
       <!-- ko with: selectedResult -->
       <div class="global-search-preview" style="overflow: auto;">
-          <div class="global-search-close-preview"><a href="javscript: void(0);" data-bind="click: function () { $parent.selectedIndex(undefined); }"><i class="fa fa-fw fa-times"></i></a></div>
+          <div class="global-search-close-preview"><a class="pointer inactive-action" data-bind="click: function () { $parent.selectedIndex(undefined); }"><i class="fa fa-fw fa-times"></i></a></div>
           <!-- ko switch: type -->
             <!-- ko case: ['database', 'document', 'field', 'table', 'view']  -->
               <!-- ko component: { name: 'context-popover-contents-global-search', params: { data: data, globalSearch: $parent } } --><!-- /ko -->

+ 9 - 7
desktop/core/src/desktop/templates/ko_components/ko_inline_autocomplete.mako

@@ -54,7 +54,6 @@ from desktop.views import _ko
         </ul>
       </div>
     </div>
-
   </script>
 
   <script type="text/javascript">
@@ -382,15 +381,18 @@ from desktop.views import _ko
 
         if (self.lastParseResult.suggestFacetValues) {
           var facetValues = ko.unwrap(self.knownFacetValues);
-          if (facetValues[self.lastParseResult.suggestFacetValues.toLowerCase()]) {
-            getSortedFacets(facetValues[self.lastParseResult.suggestFacetValues.toLowerCase()]).forEach(function (value) {
+          if (facetValues && facetValues[self.lastParseResult.suggestFacetValues.toLowerCase()]) {
+            var matchedFacets = facetValues[self.lastParseResult.suggestFacetValues.toLowerCase()];
+            getSortedFacets(matchedFacets).forEach(function (value) {
               if (value.toLowerCase().indexOf(partialLower) === 0) {
                 var fullValue = nonPartial + partial + value.substring(partial.length, value.length);
-                if (partial.length) {
-                  facetSuggestions.push({ label: '<b>' + partial + '</b>' + value.substring(partial.length), value: fullValue});
-                } else {
-                  facetSuggestions.push({ label: value, value: fullValue});
+
+                var label = partial.length ? '<b>' + partial + '</b>' + value.substring(partial.length) : value;
+
+                if (matchedFacets[value] !== -1) {
+                  label += ' (' + matchedFacets[value] + ')';
                 }
+                facetSuggestions.push({ label: label, value: fullValue });
                 newSuggestions.push(fullValue);
               }
             });

+ 10 - 6
desktop/core/src/desktop/templates/ko_components/ko_nav_tags.mako

@@ -26,10 +26,11 @@ from django.utils.translation import ugettext as _
      <!-- ko if: loading -->
      <div style="width: 100%; height: 20px; left: 6px; top: 8px; position: relative;" data-bind="hueSpinner: { spin: loading }"></div>
      <!-- /ko -->
-     <div style="width: 100%" data-bind="ifnot: loading">
+     <!-- ko if: !loading() && (!readOnly || readOnly && currentTags().length) -->
+     <div class="hue-tags"  style="width: 100%" data-bind="ifnot: loading, css: { 'read-only-tags': readOnly }">
        <textarea style="width: 100%" data-bind="tagEditor: {
           placeholder: '${_ko('No tags')}',
-          readOnly: '${ not user.has_hue_permission(action="write", app="metadata") }' === 'True',
+          readOnly: readOnly,
           hasErrors: hasErrors,
           errorMessage: '${_ko("Tags could not be loaded.")}',
           setTags: currentTags,
@@ -39,7 +40,9 @@ from django.utils.translation import ugettext as _
           load: getSelectizeTags
         }"></textarea>
        <div class="selectize-error" style="display: none;"><i class="fa fa-exclamation-triangle"></i> <span class="message"></span></div>
+       <!-- /ko -->
      </div>
+     <!-- /ko -->
   </script>
 
   <script type="text/javascript">
@@ -60,6 +63,7 @@ from django.utils.translation import ugettext as _
         self.allTags = ko.observableArray();
 
         self.catalogEntry = params.catalogEntry;
+        self.readOnly = '${ not user.has_hue_permission(action="write", app="metadata") }' === 'True' || !!params.readOnly;
 
         self.getSelectizeTags = function (query, callback) {
           callback($.map(self.allTags(), function (tag) { return { value: tag, text: tag }}));
@@ -73,7 +77,7 @@ from django.utils.translation import ugettext as _
         self.loading(true);
         self.hasErrors(false);
 
-        var currentTagsPromise = self.catalogEntry.getNavigatorMeta().done(function (navigatorMeta) {
+        var currentTagsPromise = ko.unwrap(self.catalogEntry).getNavigatorMeta().done(function (navigatorMeta) {
           self.currentTags((navigatorMeta && navigatorMeta.tags) || []);
         }).fail(function () {
           self.hasErrors(true);
@@ -116,9 +120,9 @@ from django.utils.translation import ugettext as _
         self.loading(true);
         self.hasErrors(false);
 
-        var addTagsPromise = tagsToAdd.length > 0 ? self.catalogEntry.addNavigatorTags(tagsToAdd) : $.Deferred().resolve().promise();
+        var addTagsPromise = tagsToAdd.length > 0 ? ko.unwrap(self.catalogEntry).addNavigatorTags(tagsToAdd) : $.Deferred().resolve().promise();
 
-        var deleteTagsPromise = tagsToRemove.length > 0 ? self.catalogEntry.deleteNavigatorTags(tagsToRemove) : $.Deferred().resolve().promise();
+        var deleteTagsPromise = tagsToRemove.length > 0 ? ko.unwrap(self.catalogEntry).deleteNavigatorTags(tagsToRemove) : $.Deferred().resolve().promise();
 
         addTagsPromise.fail(function (error) {
           hueUtils.logError(error);
@@ -133,7 +137,7 @@ from django.utils.translation import ugettext as _
         $.when(addTagsPromise, deleteTagsPromise).done(function () {
           if (tagsToAdd.length || tagsToRemove.length) {
             DataCatalog.updateAllNavigatorTags(tagsToAdd, tagsToRemove);
-            self.catalogEntry.save();
+            ko.unwrap(self.catalogEntry).save();
           }
           self.loading(false);
           self.loadTags();

+ 5 - 3
desktop/libs/notebook/src/notebook/connectors/hiveserver2.py

@@ -463,7 +463,7 @@ class HS2Api(Api):
   @query_error_handler
   def get_sample_data(self, snippet, database=None, table=None, column=None, async=False):
     try:
-      db = self._get_db(snippet)
+      db = self._get_db(snippet, async)
       return _get_sample_data(db, database, table, column, async)
     except QueryServerException, ex:
       raise QueryError(ex.message)
@@ -763,9 +763,11 @@ DROP TABLE IF EXISTS `%(table)s`;
     return HiveServerQueryHandle(**snippet['result']['handle'])
 
 
-  def _get_db(self, snippet):
-    if snippet['type'] == 'hive':
+  def _get_db(self, snippet, async=False):
+    if not async and snippet['type'] == 'hive':
       name = 'beeswax'
+    elif snippet['type'] == 'hive':
+      name = 'hive'
     elif snippet['type'] == 'impala':
       name = 'impala'
     else:

+ 1 - 1
desktop/libs/notebook/src/notebook/static/notebook/js/notebook.ko.js

@@ -1545,7 +1545,7 @@ var EditorViewModel = (function() {
 
     self.format = function () {
       if (self.isSqlDialect()) {
-        self.getApiHelper().formatSql(self.ace().getSelectedText() != '' ? self.ace().getSelectedText() : self.statement_raw()).done(function (data) {
+        self.getApiHelper().formatSql({ statements: self.ace().getSelectedText() != '' ? self.ace().getSelectedText() : self.statement_raw() }).done(function (data) {
           if (data.status == 0) {
             if (self.ace().getSelectedText() != '') {
               self.ace().session.replace(self.ace().session.selection.getRange(), data.formatted_statements);

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác