Переглянути джерело

HUE-9143 [frontend] Create a simplified result grid for the quick query component

This will be reused in the snippet result grid later
Johan Ahlen 5 роки тому
батько
коміт
919fc710e4

+ 367 - 0
desktop/core/src/desktop/js/apps/notebook2/components/resultGrid/ko.simpleResultGrid.js

@@ -0,0 +1,367 @@
+// 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 * as ko from 'knockout';
+
+import './ko.resultDownloadActions';
+
+import componentUtils from 'ko/components/componentUtils';
+import DisposableComponent from 'ko/components/DisposableComponent';
+import I18n from 'utils/i18n';
+import { defer, sleep, UUID } from 'utils/hueUtils';
+import {
+  HIDE_FIXED_HEADERS_EVENT,
+  REDRAW_FIXED_HEADERS_EVENT,
+  SHOW_GRID_SEARCH_EVENT,
+  SHOW_NORMAL_RESULT_EVENT
+} from 'apps/notebook2/events';
+import { trackResult } from 'apps/notebook2/components/executableStateHandler';
+import { EXECUTION_STATUS } from '../../execution/executable';
+
+export const NAME = 'simple-result-grid';
+
+// prettier-ignore
+const TEMPLATE = `
+<div class="simple-result-grid" data-bind="delayedOverflow: 'slow'">
+  <table class="table table-condensed resultTable">
+    <thead>
+    <tr data-bind="foreach: meta">
+      <th class="sorting" data-bind="
+          text: $index() === 0 ? '&nbsp;' : name,
+          css: typeof cssClass !== 'undefined' ? cssClass : 'sort-string',
+          attr: { title: type },
+          style: {
+            'width': $index() === 0 ? '1%' : '',
+            'height': $index() === 0 ? '32px' : ''
+          },
+          click: function(obj, e) {
+            $(e.target).parents('table').trigger('sort', obj);
+          }
+        "></th>
+    </tr>
+    </thead>
+    <tbody>
+    </tbody>
+  </table>
+  <div style="display:none;" data-bind="visible: isExpired && data() && data().length > 99">
+    <pre class="margin-top-10"><i class="fa fa-check muted"></i> ${ I18n('Results have expired, rerun the query if needed.') }</pre>
+  </div>
+</div>
+`;
+
+class SimpleResultGrid extends DisposableComponent {
+  constructor(params, element) {
+    super();
+    this.id = UUID();
+    this.$element = $(element);
+
+    this.activeExecutable = params.activeExecutable;
+
+    this.scrollable = params.scrollable || element.parentElement;
+    this.contained = typeof params.contained !== 'undefined' ? params.contained : true;
+
+    this.hasMore = ko.observable(false);
+    this.status = ko.observable();
+    this.meta = ko.observableArray();
+    this.data = ko.observableArray();
+    this.lastFetchedRows = ko.observableArray();
+
+    // length > 99 comes from legacy code, likely related to data fetched twice.
+    this.isExpired = ko.pureComputed(
+      () => this.status() === EXECUTION_STATUS.expired && this.data().length > 99
+    );
+
+    this.hueDatatable = undefined;
+
+    const trackerHandler = trackResult(this.activeExecutable, result => {
+      if (result) {
+        this.status(result.executable.status);
+        this.hasMore(result.hasMore);
+        if (this.meta() !== result.koEnrichedMeta) {
+          this.meta(result.koEnrichedMeta);
+          this.data(result.lastRows);
+        } else if (result.lastRows.length) {
+          this.data.push(...result.lastRows);
+        }
+        this.lastFetchedRows(result.lastRows);
+      } else {
+        this.meta([]);
+        this.data([]);
+        this.hasMore(false);
+        this.status(undefined);
+        this.lastFetchedRows([]);
+      }
+    });
+
+    this.addDisposalCallback(() => {
+      trackerHandler.dispose();
+    });
+
+    this.subscribe(this.data, this.render.bind(this));
+
+    this.subscribe(this.meta, meta => {
+      if (meta) {
+        if (this.hueDatatable) {
+          this.hueDatatable.fnDestroy();
+          this.hueDatatable = undefined;
+        }
+
+        const $resultTable = this.getResultTableElement();
+        if ($resultTable.data('plugin_jHueTableExtender2')) {
+          $resultTable.data('plugin_jHueTableExtender2').destroy();
+        }
+        $resultTable.data('scrollToCol', null);
+        $resultTable.data('scrollToRow', null);
+        $resultTable.data('rendered', false);
+        if ($resultTable.hasClass('dt')) {
+          $resultTable.removeClass('dt');
+          $resultTable.find('thead tr').empty();
+          $resultTable.data('lockedRows', {});
+        }
+      }
+    });
+
+    this.disposals.push(() => {
+      if (this.hueDatatable) {
+        this.hueDatatable.fnDestroy();
+      }
+    });
+  }
+
+  createHueDatatable() {
+    let invisibleRows = 10;
+    if (this.data().length) {
+      const colCount = this.data()[0].length;
+      invisibleRows = colCount > 200 ? 10 : colCount > 30 ? 50 : 100;
+    }
+    const $datatablesWrapper = this.getWrapperElement();
+
+    const hueDatatable = this.getResultTableElement().hueDataTable({
+      i18n: {
+        NO_RESULTS: I18n('No results found.'),
+        OF: I18n('of')
+      },
+      fnDrawCallback: () => {
+        const $resultTable = this.getResultTableElement();
+        $('#queryResults').removeAttr('style');
+        $datatablesWrapper.css('overflow-x', 'hidden');
+        $resultTable.jHueHorizontalScrollbar();
+        $datatablesWrapper.jHueScrollLeft();
+      },
+      scrollable: this.scrollable,
+      contained: this.contained,
+      forceInvisible: invisibleRows
+    });
+
+    this.disposals.push(() => {
+      hueDatatable.fnDestroy();
+    });
+
+    defer(() => {
+      const tableExtenderOptions = {
+        mainScrollable: this.scrollable,
+        fixedFirstColumn: true,
+        parentId: this.id,
+        clonedContainerPosition: 'fixed',
+        app: 'editor'
+      };
+      this.getWrapperElement().css('overflow-x', 'hidden');
+      const bannerTopHeight = window.BANNER_TOP_HTML ? 30 : 2;
+      tableExtenderOptions.stickToTopPosition = 48 + bannerTopHeight;
+
+      const $resultTable = this.getResultTableElement();
+
+      $resultTable.jHueTableExtender2(tableExtenderOptions);
+      this.disposals.push(() => {
+        if ($resultTable.data('plugin_jHueTableExtender2')) {
+          $resultTable.data('plugin_jHueTableExtender2').destroy();
+        }
+      });
+
+      $resultTable.jHueHorizontalScrollbar();
+    });
+
+    return hueDatatable;
+  }
+
+  createDatatable() {
+    const $resultTable = this.getResultTableElement();
+    const $resultTableParent = $resultTable.parent();
+    // When executing few columns -> many columns -> few columns we have to clear the style
+    $resultTable.removeAttr('style');
+    if ($resultTable.hasClass('table-huedatatable')) {
+      $resultTable.removeClass('table-huedatatable');
+      if ($resultTableParent.hasClass('dataTables_wrapper')) {
+        $resultTable.unwrap();
+      }
+    }
+    $resultTable.addClass('dt');
+
+    this.hueDatatable = this.createHueDatatable($resultTable);
+
+    //const $dataTablesWrapper = $resultTable.parents('.dataTables_wrapper');
+
+    const $scrollElement = $(this.scrollable);
+
+    let scrollThrottle = -1;
+
+    this.disposals.push(() => {
+      window.clearTimeout(scrollThrottle);
+    });
+
+    let fetchingRows = false;
+    const dataScroll = () => {
+      if (!$resultTable.is(':visible')) {
+        return;
+      }
+
+      let lastScrollPosition =
+        $scrollElement.data('scrollPosition') != null ? $scrollElement.data('scrollPosition') : 0;
+      window.clearTimeout(scrollThrottle);
+      $scrollElement.data('scrollPosition', $scrollElement.scrollTop());
+      scrollThrottle = window.setTimeout(async () => {
+        lastScrollPosition--; //hack for forcing fetching
+        if (
+          lastScrollPosition !== $scrollElement.scrollTop() &&
+          $scrollElement.scrollTop() + $scrollElement.outerHeight() + 20 >=
+            $scrollElement[0].scrollHeight &&
+          this.hueDatatable &&
+          this.hasMore() &&
+          !fetchingRows
+        ) {
+          this.showGrayedOutResult();
+          fetchingRows = true;
+          try {
+            await this.fetchRows(100, false);
+          } finally {
+            fetchingRows = false;
+          }
+        }
+      }, 100);
+    };
+
+    $scrollElement.off('scroll.' + this.id);
+    $scrollElement.on('scroll.' + this.id, dataScroll);
+    this.disposals.push(() => {
+      $scrollElement.off('scroll.' + this.id);
+    });
+
+    this.subscribe(REDRAW_FIXED_HEADERS_EVENT, this.redrawFixedHeaders.bind(this));
+
+    this.subscribe(HIDE_FIXED_HEADERS_EVENT, this.hideFixedHeaders.bind(this));
+
+    this.subscribe(SHOW_NORMAL_RESULT_EVENT, this.showNormalResult.bind(this));
+
+    return this.hueDatatable;
+  }
+
+  async fetchRows() {
+    if (this.activeExecutable() && this.activeExecutable().result) {
+      await this.activeExecutable().result.fetchRows({ rows: 100 });
+    }
+  }
+
+  getResultTableElement() {
+    return this.$element.find('.resultTable');
+  }
+
+  getWrapperElement() {
+    return this.$element.find('.dataTables_wrapper');
+  }
+
+  hideFixedHeaders() {
+    const $component = this.$element;
+    $component.find('.jHueTableExtenderClonedContainer').hide();
+    $component.find('.jHueTableExtenderClonedContainerColumn').hide();
+    $component.find('.jHueTableExtenderClonedContainerCell').hide();
+    $component.find('.fixed-header-row').hide();
+    $component.find('.fixed-first-cell').hide();
+    $component.find('.fixed-first-column').hide();
+  }
+
+  showFixedHeaders() {
+    const $component = this.$element;
+    $component.find('.jHueTableExtenderClonedContainer').show();
+    $component.find('.jHueTableExtenderClonedContainerColumn').show();
+    $component.find('.jHueTableExtenderClonedContainerCell').show();
+    $component.find('.fixed-header-row').show();
+    $component.find('.fixed-first-cell').show();
+    $component.find('.fixed-first-column').show();
+  }
+
+  redrawFixedHeaders() {
+    if (this.meta().length > 0) {
+      const tableExtender = this.getResultTableElement().data('plugin_jHueTableExtender2');
+      if (tableExtender) {
+        tableExtender.repositionHeader();
+        tableExtender.drawLockedRows();
+      }
+      const $scrollable = $(this.scrollable);
+      $scrollable.data('lastScroll', $scrollable.scrollTop());
+      $scrollable.trigger('scroll');
+    }
+    this.showFixedHeaders();
+  }
+
+  async render() {
+    if (this.data().length || this.lastFetchedRows().length) {
+      await sleep(300);
+      const $resultTable = this.getResultTableElement();
+      const initial = !$resultTable.data('rendered');
+      let dataTable;
+      if (initial) {
+        //this.$element.find('select').trigger('chosen:updated'); // TODO: Extract
+        dataTable = this.createDatatable();
+        $resultTable.data('rendered', true);
+      } else {
+        dataTable = $resultTable.hueDataTable();
+      }
+      try {
+        dataTable.fnAddData(initial && this.data().length ? this.data() : this.lastFetchedRows());
+      } catch (e) {}
+      const $dataTablesWrapper = this.getWrapperElement();
+      this.showNormalResult();
+      $dataTablesWrapper.scrollTop($dataTablesWrapper.data('scrollPosition'));
+    } else {
+      this.showNormalResult();
+    }
+  }
+
+  showGrayedOutResult() {
+    const $wrapper = this.getWrapperElement();
+    $wrapper.find('.fixed-first-column').css({ opacity: '0' });
+    $wrapper.find('.fixed-header-row').css({ opacity: '0' });
+    $wrapper.find('.fixed-first-cell').css({ opacity: '0' });
+    $wrapper.find('.resultTable').css({ opacity: '0.55' });
+  }
+
+  showNormalResult() {
+    const $wrapper = this.getWrapperElement();
+    $wrapper.find('.fixed-first-column').css({ opacity: '1' });
+    $wrapper.find('.fixed-header-row').css({ opacity: '1' });
+    $wrapper.find('.fixed-first-cell').css({ opacity: '1' });
+    $wrapper.find('.resultTable').css({ opacity: '1' });
+  }
+}
+
+componentUtils.registerComponent(
+  NAME,
+  {
+    createViewModel: (params, componentInfo) => new SimpleResultGrid(params, componentInfo.element)
+  },
+  TEMPLATE
+);

+ 10 - 3
desktop/core/src/desktop/js/ko/components/contextPopover/ko.quickQueryContext.js

@@ -20,6 +20,7 @@ import { MULTI_NAME as SIMPLE_ACE_MULTI } from 'ko/components/simpleAceEditor/ko
 import { NAME as CONTEXT_SELECTOR } from 'ko/components/ko.contextSelector';
 import { NAME as DROP_DOWN } from 'ko/components/ko.dropDown';
 import { NAME as EXECUTABLE_ACTIONS } from 'apps/notebook2/components/ko.executableActions';
+import { NAME as SIMPLE_RESULT_GRID } from 'apps/notebook2/components/resultGrid/ko.simpleResultGrid';
 
 import componentUtils from 'ko/components/componentUtils';
 import DisposableComponent from 'ko/components/DisposableComponent';
@@ -37,7 +38,7 @@ const TEMPLATE = `
   <!-- ko hueSpinner: { spin: loadingConfig, center: true, size: 'xlarge' } --><!-- /ko -->
   <!-- ko ifnot: loadingConfig -->
     <div style="display: inline-block" data-bind="
-      component: { 
+      component: {
         name: '${ DROP_DOWN }',
         params: {
           value: interpreter,
@@ -65,7 +66,7 @@ const TEMPLATE = `
     <!-- ko ifnot: loadingContext -->
       <!-- ko with: interpreter -->
         <div style="margin: 10px;" data-bind="
-          component: { 
+          component: {
             name: '${ SIMPLE_ACE_MULTI }',
             params: {
               autocomplete: $parent.autocomplete,
@@ -87,7 +88,13 @@ const TEMPLATE = `
         <div data-bind="
           component: {
             name: '${ EXECUTABLE_ACTIONS }',
-            params: { activeExecutable: $parent.activeExecutable } 
+            params: { activeExecutable: $parent.activeExecutable }
+          }
+        "></div>
+        <div data-bind="
+          component: {
+            name: '${ SIMPLE_RESULT_GRID }',
+            params: { activeExecutable: $parent.activeExecutable }
           }
         "></div>
       <!-- /ko -->