|
@@ -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 ? ' ' : 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
|
|
|
|
|
+);
|