瀏覽代碼

HUE-7467 [frontend] Create Solr sample popup

Enrico Berti 8 年之前
父節點
當前提交
6df0f9e

+ 107 - 0
desktop/core/src/desktop/static/desktop/js/apiHelper.js

@@ -65,6 +65,8 @@ var ApiHelper = (function () {
   var CONFIG_APPS_API = '/desktop/api/configurations';
   var SOLR_COLLECTIONS_API = '/indexer/api/indexes/list/';
   var SOLR_FIELDS_API = '/indexer/api/index/list/';
+  var DASHBOARD_TERMS_API = '/dashboard/get_terms';
+  var DASHBOARD_STATS_API = '/dashboard/get_stats';
 
   var HBASE_API_PREFIX = '/hbase/api/';
   var SAVE_TO_FILE = '/filebrowser/save';
@@ -689,6 +691,111 @@ var ApiHelper = (function () {
     }));
   };
 
+  /**
+   * @param {Object} options
+   * @param {String} options.collectionName
+   * @param {String} options.fieldName
+   * @param {String} options.prefix
+   * @param {String} [options.engine]
+   * @param {Function} options.successCallback
+   * @param {Function} [options.alwaysCallback]
+   * @param {Number} [options.timeout]
+   *
+   */
+  ApiHelper.prototype.fetchDashboardTerms = function (options) {
+    var self = this;
+    if (options.timeout === 0) {
+      self.assistErrorCallback(options)({ status: -1 });
+      return;
+    }
+    $.ajax({
+      dataType: 'json',
+      url: DASHBOARD_TERMS_API,
+      type: 'POST',
+      data: {
+        collection: ko.mapping.toJSON({
+          id: '',
+          name: options.collectionName,
+          engine: options.engine || 'solr'
+        }),
+        analysis: ko.mapping.toJSON({
+          name: options.fieldName,
+          terms: {
+            prefix: options.prefix || ''
+          }
+        })
+      },
+      timeout: options.timeout,
+      success: function (data) {
+        if (!data.error && !self.successResponseIsError(data) && data.status === 0) {
+          options.successCallback(data);
+        } else {
+          self.assistErrorCallback(options)(data);
+        }
+      }
+    })
+    .fail(self.assistErrorCallback(options))
+    .always(options.alwaysCallback);
+  };
+
+  /**
+   * @param {Object} options
+   * @param {String} options.collectionName
+   * @param {String} options.fieldName
+   * @param {String} [options.engine]
+   * @param {Function} options.successCallback
+   * @param {Function} [options.alwaysCallback]
+   * @param {Number} [options.timeout]
+   *
+   */
+  ApiHelper.prototype.fetchDashboardStats = function (options) {
+    var self = this;
+    if (options.timeout === 0) {
+      self.assistErrorCallback(options)({ status: -1 });
+      return;
+    }
+    $.ajax({
+      dataType: 'json',
+      url: DASHBOARD_STATS_API,
+      type: 'POST',
+      data: {
+        collection: ko.mapping.toJSON({
+          id: '',
+          name: options.collectionName,
+          engine: options.engine || 'solr'
+        }),
+        analysis: ko.mapping.toJSON({
+          name: options.fieldName,
+          stats: {
+            facet: ''
+          },
+        }),
+        query: ko.mapping.toJSON({
+          qs: [{q: ''}],
+          fqs: []
+        })
+      },
+      timeout: options.timeout,
+      success: function (data) {
+        if (!data.error && !self.successResponseIsError(data) && data.status === 0) {
+          if (data.status === 0){
+            options.successCallback(data);
+          }
+          else if (data.status === 1) {
+            options.notSupportedCallback(data);
+          }
+          else {
+            self.assistErrorCallback(options)(data);
+          }
+        } else {
+          self.assistErrorCallback(options)(data);
+        }
+      }
+    })
+    .fail(self.assistErrorCallback(options))
+    .always(options.alwaysCallback);
+  };
+
   /**
    * @param {Object} options
    * @param {Function} options.successCallback

+ 28 - 0
desktop/core/src/desktop/static/desktop/js/assist/assistCollectionEntry.js

@@ -121,6 +121,34 @@ var AssistCollectionEntry = (function () {
 
   };
 
+  AssistCollectionEntry.prototype.showContextPopover = function (entry, event, positionAdjustment) {
+    var self = this;
+    var $source = $(event.target);
+    var offset = $source.offset();
+    if (positionAdjustment) {
+      offset.left += positionAdjustment.left;
+      offset.top += positionAdjustment.top;
+    }
+
+    huePubSub.publish('context.popover.show', {
+      data: {
+        type: 'collection',
+        definition: self.definition,
+        parent: self.parent
+      },
+      showInAssistEnabled: false,
+      orientation: 'right',
+      pinEnabled: false,
+      source: {
+        element: event.target,
+        left: offset.left,
+        top: offset.top - 3,
+        right: offset.left + $source.width() + 1,
+        bottom: offset.top + $source.height() - 3
+      }
+    });
+  };
+
   AssistCollectionEntry.prototype.openInBrowser = function () {
     huePubSub.publish('assist.openCollectionItem', this);
   };

+ 1 - 1
desktop/core/src/desktop/templates/assist.mako

@@ -693,7 +693,7 @@ from notebook.conf import ENABLE_QUERY_BUILDER, ENABLE_QUERY_SCHEDULING, get_ord
       <div data-bind="visible: ! loading() && ! hasErrors()" style="position: relative;">
         <ul class="assist-tables" data-bind="foreachVisible: { data: filteredEntries, minHeight: 22, container: '.assist-collections-scrollable' }">
           <li class="assist-entry assist-table-link" style="position: relative;" data-bind="appAwareTemplateContextMenu: { template: 'collections-context-items', scrollContainer: '.assist-collections-scrollable' }, visibleOnHover: { 'selector': '.assist-actions' }">
-            <a href="javascript:void(0)" class="assist-entry assist-table-link" data-bind="multiClick: { click: click, dblClick: dblClick }, attr: {'title': definition.name + (definition.type != 'collection' && definition.type != 'alias' ? ' - ' + definition.type : '') }">
+            <a href="javascript:void(0)" class="assist-entry assist-table-link" data-bind="click: function (data, event) { definition.type == 'collection' ? click() : showContextPopover(data, event); }, attr: {'title': definition.name + (definition.type != 'collection' && definition.type != 'alias' ? ' - ' + definition.type : '') }">
               <!-- ko switch: definition.type -->
                 <!-- ko case: 'collection' -->
                 <i class="fa fa-fw fa-search muted valign-middle"></i>

+ 188 - 0
desktop/core/src/desktop/templates/context_popover.mako

@@ -220,6 +220,87 @@ from metadata.conf import has_navigator
     </div>
   </script>
 
+  <script type="text/html" id="context-popover-collection-stats-details">
+    <div class="context-popover-flex-fill" data-bind="niceScroll">
+      <div style="padding: 8px">
+        <div data-bind="with: stats">
+        <!-- ko hueSpinner: { spin:  $parent.loadingStats, center: true, size: 'large' } --><!-- /ko -->
+        <div class="alert" data-bind="visible: !$parent.loadingStats() && !$parent.statsSupported()">${ _('This field does not support stats') }</div>
+        <div class="alert" data-bind="visible: !$parent.loadingStats() && $data.data().length == 0">${ _('There are no stats to be shown') }</div>
+        <table style="width: 100%" data-bind="visible: !$parent.loadingStats() && $data.data().length > 0" class="table table-condensed">
+          <tbody data-bind="foreach: $data.data">
+          <tr>
+            <td style="vertical-align: top"><strong data-bind="text: key"></strong></td>
+            <!-- ko if: key == 'facets' -->
+            <td>
+              <!-- ko if: val[Object.keys(val)[0]] != null -->
+              <table>
+                <tbody data-bind="foreach: Object.keys(val[Object.keys(val)[0]])">
+                  <tr>
+                    <td style="vertical-align: top; padding-left: 4px; padding-right: 4px"><strong data-bind="text: $data"></strong></td>
+                    <td data-bind="template: 'context-popover-collection-stats-facets'"></td>
+                  </tr>
+                </tbody>
+              </table>
+              <!-- /ko -->
+              <!-- ko ifnot: val[Object.keys(val)[0]] != null -->
+              ${ _('Not available') }
+              <!-- /ko -->
+            </td>
+            <!-- /ko -->
+            <!-- ko ifnot: key == 'facets' -->
+            <td data-bind="text: val"></td>
+            <!-- /ko -->
+          </tr>
+          </tbody>
+        </table>
+        </div>
+      </div>
+    </div>
+  </script>
+
+  <script type="text/html" id="context-popover-collection-stats-facets">
+    <table style="width: 100%">
+      <tbody data-bind="foreach: Object.keys($parent.val[Object.keys($parent.val)[0]][$data])">
+        <tr>
+          <td style="vertical-align: top; padding-left: 4px; padding-right: 4px"><strong data-bind="text: $data"></strong></td>
+          <!-- ko ifnot: $data == 'facets' -->
+          <td style="vertical-align: top" data-bind="text: $parents[1].val[Object.keys($parents[1].val)[0]][$parent][$data]"></td>
+          <!-- /ko -->
+        </tr>
+      </tbody>
+    </table>
+  </script>
+
+
+  <script type="text/html" id="context-popover-collection-terms-details">
+    <div class="context-popover-flex-fill" data-bind="niceScroll">
+      <div style="padding: 8px">
+        <div data-bind="with: terms">
+          <!-- ko hueSpinner: { spin:  $parent.loadingTerms, center: true, size: 'large' } --><!-- /ko -->
+          <div class="pull-right" data-bind="visible: !$parent.loadingTerms()">
+            <label class="muted margin-left-5 margin-bottom-10">${ _('Prefix filter') } <input type="text" data-bind="value: prefix, clearable: prefix, valueUpdate:'afterkeydown'" placeholder="${ _('Filter...') }" class="margin-left-10 no-margin-bottom"></label>
+          </div>
+          <div class="clearfix"></div>
+          <div class="alert" data-bind="visible: !$parent.loadingTerms() && $data.data().length == 0">${ _('There are no terms to be shown') }</div>
+          <table style="width: 100%" data-bind="visible: !$parent.loadingTerms() && $data.data().length > 0" class="table table-condensed">
+            <tbody data-bind="foreach: $data.data">
+            <tr>
+              <td data-bind="text: val.value"></td>
+              <td style="width: 40px">
+                <div class="progress">
+                  <div class="bar-label" data-bind="text:val.count"></div>
+                  <div class="bar bar-info" style="margin-top:-20px;" data-bind="style: {'width': ((val.count / $parent.data()[0].val.count) * 100) + '%'}"></div>
+                </div>
+              </td>
+            </tr>
+            </tbody>
+          </table>
+        </div>
+      </div>
+    </div>
+  </script>
+
   <script type="text/html" id="context-popover-asterisk-details">
     <div class="context-popover-flex-fill">
       <!-- ko component: { name: 'sql-columns-table', params: { columns: columns, scrollToColumns: false } } --><!-- /ko -->
@@ -328,6 +409,9 @@ from metadata.conf import has_navigator
       <div class="hue-popover-arrow" data-bind="style: { 'margin-left': leftAdjust() + 'px',  'margin-top': topAdjust() + 'px' }"></div>
       <div class="hue-popover-title" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-right: 30px;">
         <i class="fa muted" data-bind="css: iconClass" style="margin-top: 3px"></i> <span style="padding-left: 4px;" data-bind="text: title"></span>
+        <!-- ko if: subtitle -->
+        <span class="muted" style="padding-left: 4px;" data-bind="html: subtitle"></span>
+        <!-- /ko -->
         <div style="position: absolute; right: 6px; top: 8px;">
           <a class="pointer inactive-action" data-bind="visible: pinEnabled, click: pin"><i class="fa fa-fw fa-thumb-tack"></i></a>
           <a class="pointer inactive-action" data-bind="click: close"><i class="fa fa-fw fa-times"></i></a>
@@ -1087,6 +1171,104 @@ from metadata.conf import has_navigator
         })
       };
 
+      function CollectionContextTabs(data) {
+        var self = this;
+
+        self.apiHelper = ApiHelper.getInstance();
+
+        self.disposals = [];
+
+        self.data = ko.observable({
+          details: data,
+          loading: ko.observable(false),
+          hasErrors: ko.observable(false),
+          selectedPath: ko.observable(data.path),
+          loadingTerms: ko.observable(false),
+          loadingStats: ko.observable(false),
+          statsSupported: ko.observable(true),
+          terms: ko.mapping.fromJS({'prefix': '', 'data': []}),
+          stats: ko.mapping.fromJS({'facet': '', 'data': []})
+        });
+
+        self.data().terms.prefix.subscribe(function () {
+          self.loadTerms();
+        });
+        self.data().terms.prefix.extend({rateLimit: {timeout: 500, method: "notifyWhenChangesStop"}});
+
+        self.loadTerms();
+        self.loadStats();
+
+        self.tabs = [
+          {
+            id: 'terms',
+            label: '${ _("Terms") }',
+            template: 'context-popover-collection-terms-details',
+            templateData: self.data
+          },
+          {
+            id: 'stats',
+            label: '${ _("Stats") }',
+            template: 'context-popover-collection-stats-details',
+            templateData: self.data
+          }
+        ];
+        self.activeTab = ko.observable('terms');
+      }
+
+      CollectionContextTabs.prototype.loadTerms = function () {
+        var self = this;
+        self.data().terms.data.removeAll();
+        self.data().loadingTerms(true);
+        self.apiHelper.fetchDashboardTerms({
+          collectionName: self.data().details.parent.definition.name,
+          fieldName: self.data().details.definition.name,
+          prefix: self.data().terms.prefix(),
+          engine: 'solr',
+          successCallback: function (data) {
+            if (data.terms != null) {
+              $.each(data.terms, function (key, val) {
+                self.data().terms.data.push({'key': key, 'val': val});
+              });
+            }
+          },
+          alwaysCallback: function () {
+            self.data().loadingTerms(false);
+          }
+        });
+      };
+
+      CollectionContextTabs.prototype.loadStats = function () {
+        var self = this;
+        self.data().terms.data.removeAll();
+        self.data().loadingStats(true);
+        self.data().statsSupported(true);
+        self.apiHelper.fetchDashboardStats({
+          collectionName: self.data().details.parent.definition.name,
+          fieldName: self.data().details.definition.name,
+          engine: 'solr',
+          successCallback: function (data) {
+            if (data.stats.stats.stats_fields[self.data().details.definition.name] != null) {
+              $.each(data.stats.stats.stats_fields[self.data().details.definition.name], function (key, val) {
+                self.data().stats.data.push({'key': key, 'val': val});
+              });
+            }
+          },
+          notSupportedCallback: function () {
+            self.data().statsSupported(false);
+          },
+          alwaysCallback: function () {
+            self.data().loadingStats(false);
+          }
+        });
+      };
+
+      CollectionContextTabs.prototype.dispose = function () {
+        var self = this;
+        while (self.disposals.length) {
+          self.disposals.pop()();
+        }
+      };
+
       function ResizeHelper (orientation, leftAdjust, topAdjust) {
         var self = this;
 
@@ -1332,6 +1514,7 @@ from metadata.conf import has_navigator
         self.isAsterisk = params.data.type === 'asterisk';
         self.isView = params.data.type === 'view';
         self.isDocument = params.data.type.toLowerCase() === 'hue';
+        self.isCollection = params.data.type === 'collection';
 
         self.showInAssistEnabled = (typeof params.showInAssistEnabled !== 'undefined' ? params.showInAssistEnabled : true) && (self.isDocument || self.isDatabase || self.isTable || self.isColumn);
         self.openInDashboardEnabled = self.isTable || self.isView || self.isDatabase;
@@ -1404,6 +1587,11 @@ from metadata.conf import has_navigator
           self.contents = new DocumentContext(self.data.definition);
           self.title = self.data.definition.name;
           self.iconClass = 'fa-file-o';
+        } else if (self.isCollection) {
+          self.contents = new CollectionContextTabs(self.data);
+          self.title = self.data.parent.definition.name + '.' + self.data.definition.name;
+          self.subtitle = (self.data.parent.key() === self.data.definition.name ? '<i class="fa fa-key"></i> (' : '(') + self.data.definition.type + ')';
+          self.iconClass = 'fa-search';
         } else {
           self.title = '';
           self.iconClass = 'fa-info'