Selaa lähdekoodia

HUE-9000 [editor] Add copy result to clipboard action in editor v2

This also adds the skeleton for the various export options
Johan Ahlen 6 vuotta sitten
vanhempi
commit
46ce0c6800

+ 185 - 0
desktop/core/src/desktop/js/apps/notebook2/components/resultGrid/ko.resultDownloadActions.js

@@ -0,0 +1,185 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import componentUtils from 'ko/components/componentUtils';
+import DisposableComponent from 'ko/components/DisposableComponent';
+import I18n from 'utils/i18n';
+import hueUtils from 'utils/hueUtils';
+
+export const NAME = 'result-download-actions';
+
+// prettier-ignore
+const TEMPLATE = `
+<div class="btn-group">
+  <button class="btn btn-mini btn-editor dropdown-toggle" data-toggle="dropdown">
+      <!-- ko ifnot: isDownloading -->
+      <i class="fa fa-fw fa-download"></i>
+      <!-- /ko -->
+      <!-- ko if: isDownloading -->
+      <i class="fa fa-fw fa-spinner fa-spin"></i>
+      <!-- /ko -->
+      ${ I18n('Export') }
+    <span class="caret"></span>
+  </button>
+  <ul class="dropdown-menu">
+    <li style="display: none;">
+      <a href="javascript:void(0)" data-bind="
+          click: downloadCsv,
+          event: { mouseover: mouseOver, mouseout: mouseOut },
+          attr: { 'title': downloadCsvTitle }
+        ">
+        <i class="fa fa-fw fa-file-o"></i> ${ I18n('CSV') }
+      </a>
+    </li>
+    <li style="display: none;">
+      <a href="javascript:void(0)" data-bind="
+          click: downloadXls,
+          event: { mouseover: mouseOver, mouseout: mouseOut },
+          attr: { 'title': downloadXlsTitle }
+        ">
+        <i class="fa fa-fw fa-file-excel-o"></i> ${ I18n('Excel') }
+      </a>
+    </li>
+    <li>
+      <a href="javascript:void(0)" title="${ I18n('Copy the displayed results to your clipboard') }" data-bind="
+          clipboard: {
+            target: clipboardTarget.bind($data),
+            onSuccess: onClipboardSuccess.bind($data)
+          }
+        ">
+        <i class="fa fa-fw fa-clipboard"></i> ${ I18n('Clipboard') }
+      </a>
+    </li>
+    <!-- ko if: window.ENABLE_SQL_INDEXER -->
+    <li style="display: none;">
+      <a class="download" href="javascript:void(0)" data-bind="click: exportToReport" title="${ I18n('Visually explore the result') }">
+        <!-- ko template: { name: 'app-icon-template', data: { icon: 'report' } } --><!-- /ko --> ${ I18n('Report') }
+      </a>
+    </li>
+    <li style="display: none;">
+      <a class="download" href="javascript:void(0)" data-bind="click: exportToDashboard" title="${ I18n('Visually explore the result') }">
+        <!-- ko template: { name: 'app-icon-template', data: { icon: 'dashboard' } } --><!-- /ko --> ${ I18n('Dashboard') }
+      </a>
+    </li>
+    <!-- /ko -->
+    <li style="display: none;">
+      <a class="download" href="javascript:void(0)" data-bind="click: exportToFile" title="${ I18n('Export the result into a file, an index, a new table...') }">
+        <i class="fa fa-fw fa-cloud-upload"></i> ${ I18n('File') }
+      </a>
+    </li>
+  </ul>
+</div>
+`;
+
+class ResultDownloadActions extends DisposableComponent {
+  constructor(params) {
+    super();
+
+    this.data = params.data;
+    this.meta = params.meta;
+
+    this.isDownloading = ko.observable(false);
+
+    this.downloadCsvTitle = '';
+    this.downloadXlsTitle = '';
+
+    const rowLimit = window.DOWNLOAD_ROW_LIMIT;
+    const mbLimit = window.DOWNLOAD_BYTES_LIMIT && window.DOWNLOAD_BYTES_LIMIT / 1024 / 1024;
+    if (typeof rowLimit !== 'undefined' && typeof mbLimit !== 'undefined') {
+      this.downloadCsvTitle = I18n('Download first %s rows or %s MB as CSV', rowLimit, mbLimit);
+      this.downloadXlsTitle = I18n('Download first %s rows or %s MB as XLS', rowLimit, mbLimit);
+    } else if (typeof mbLimit !== 'undefined') {
+      this.downloadCsvTitle = I18n('Download first %s MB as CSV', mbLimit);
+      this.downloadXlsTitle = I18n('Download first %s MB as XLS', mbLimit);
+    } else if (typeof rowLimit !== 'undefined') {
+      this.downloadCsvTitle = I18n('Download first %s rows as CSV', rowLimit);
+      this.downloadXlsTitle = I18n('Download first %s rows as XLS', rowLimit);
+    }
+
+    let previousOnBeforeUnload = window.onbeforeunload;
+    this.mouseOver = () => {
+      previousOnBeforeUnload = window.onbeforeunload;
+      window.onbeforeunload = undefined;
+    };
+
+    this.mouseOut = () => {
+      window.onbeforeunload = previousOnBeforeUnload;
+    };
+  }
+
+  clipboardTarget() {
+    const $clipboardContent = $('.clipboard-content');
+
+    if (this.meta().length) {
+      let htmlString = '<table><tr>';
+      this.meta().forEach((metaCol, idx) => {
+        // Skip the row number column
+        if (idx > 0) {
+          htmlString += `<th>${hueUtils.html2text(metaCol.name)}</th>`;
+        }
+      });
+      htmlString += '</tr>';
+      this.data().forEach(row => {
+        htmlString += '<tr>';
+        row.forEach((cell, idx) => {
+          // Skip the row number column
+          if (idx > 0) {
+            htmlString += `<td>${hueUtils.html2text(cell)}</td>`;
+          }
+        });
+        htmlString += '</tr>';
+      });
+      $clipboardContent.html(htmlString);
+    } else {
+      $clipboardContent.html(I18n('Error while copying results.'));
+    }
+    return $clipboardContent[0];
+  }
+
+  onClipboardSuccess(e) {
+    $.jHueNotify.info(I18n('%s result(s) copied to the clipboard', this.data().length));
+    e.clearSelection();
+    $('.clipboard-content').empty();
+  }
+
+  downloadCsv() {}
+
+  downloadXls() {}
+
+  exportToDashboard() {
+    // saveTarget('dashboard');
+    // trySaveResults();
+  }
+
+  exportToFile() {
+    // savePath('');
+    // $('#saveResultsModal' + snippet.id()).modal('show');
+  }
+
+  exportToReport() {
+    // saveTarget('dashboard');
+    // if (notebook.canSave()) {
+    //   notebook.save()
+    // } else {
+    //   $('#saveAsModaleditor').modal('show');
+    // }
+  }
+}
+
+componentUtils.registerComponent(NAME, ResultDownloadActions, TEMPLATE);

+ 17 - 4
desktop/core/src/desktop/js/apps/notebook2/components/resultGrid/ko.resultGrid.js

@@ -17,6 +17,8 @@
 import $ from 'jquery';
 import ko from 'knockout';
 
+import './ko.resultDownloadActions';
+
 import componentUtils from 'ko/components/componentUtils';
 import DisposableComponent from 'ko/components/DisposableComponent';
 import I18n from 'utils/i18n';
@@ -28,6 +30,7 @@ import {
   SHOW_GRID_SEARCH_EVENT,
   SHOW_NORMAL_RESULT_EVENT
 } from 'apps/notebook2/events';
+
 import { attachTracker } from 'apps/notebook2/components/executableStateHandler';
 
 export const NAME = 'result-grid';
@@ -36,15 +39,25 @@ export const NAME = 'result-grid';
 const TEMPLATE = `
 <div class="result-actions-append">
   <div class="btn-group">
-    <button class="btn btn-editor btn-mini disable-feedback" data-bind="click: showSearch.bind($data), css: { 'disabled': !data().length }">
-      <i class="fa fa-search"></i> ${ I18n('Search') }
+    <button class="btn btn-editor btn-mini disable-feedback" data-bind="toggle: columnsVisible, css: { 'active' : columnsVisible }">
+      <i class="fa fa-columns"></i> ${ I18n('Columns') }
     </button>
   </div>
+
   <div class="btn-group">
-    <button class="btn btn-editor btn-mini disable-feedback" data-bind="toggle: columnsVisible, css: { 'active' : columnsVisible }">
-      <i class="fa fa-columns"></i> ${ I18n('Columns') }
+    <button class="btn btn-editor btn-mini disable-feedback" data-bind="click: showSearch.bind($data), css: { 'disabled': !data().length }">
+      <i class="fa fa-search"></i> ${ I18n('Search') }
     </button>
   </div>
+
+  <!-- ko component: {
+    name: 'result-download-actions',
+    params: {
+      meta: meta,
+      data: data
+    }
+  } --><!-- /ko -->
+
   <!-- ko if: false && window.ENABLE_DOWNLOAD -->
     <div data-bind="
         component: {

+ 40 - 0
desktop/core/src/desktop/js/ko/bindings/ko.clipboard.js

@@ -0,0 +1,40 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import ko from 'knockout';
+
+ko.bindingHandlers.clipboard = {
+  init: (element, valueAccessor) => {
+    if (!window.Clipboard) {
+      console.warn('ko.clipboard.js depends on window.Clipboard');
+      return;
+    }
+    const options = valueAccessor();
+
+    const clipboard = new window.Clipboard(element, {
+      target: options.target,
+      text: options.text
+    });
+
+    if (options.onSuccess) {
+      clipboard.on('success', options.onSuccess);
+    }
+
+    ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
+      clipboard.destroy();
+    });
+  }
+};

+ 1 - 0
desktop/core/src/desktop/js/ko/ko.all.js

@@ -50,6 +50,7 @@ import 'ko/bindings/ko.chosen';
 import 'ko/bindings/ko.clearable';
 import 'ko/bindings/ko.clickForAceFocus';
 import 'ko/bindings/ko.clickToCopy';
+import 'ko/bindings/ko.clipboard';
 import 'ko/bindings/ko.codemirror';
 import 'ko/bindings/ko.contextMenu';
 import 'ko/bindings/ko.contextSubMenu';

+ 6 - 2
desktop/core/src/desktop/js/utils/i18n.js

@@ -14,14 +14,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-const I18n = identifier => {
+const I18n = (identifier, ...replacements) => {
   if (window.DJANGO_DEBUG_MODE && !HUE_I18n[identifier]) {
     if (!window.missing_I18n) {
       window.missing_I18n = [];
     }
     window.missing_I18n.push(`'${identifier}': '\${ _('${identifier}') }',`);
   }
-  return HUE_I18n[identifier] || identifier;
+  let result = HUE_I18n[identifier] || identifier;
+  replacements.forEach(replacement => {
+    result = result.replace('%s', replacement);
+  });
+  return result;
 };
 
 export default I18n;

+ 7 - 3
desktop/core/src/desktop/templates/global_js_constants.mako

@@ -18,18 +18,18 @@
   from django.utils.translation import ugettext as _
 
   from desktop import conf
-  from desktop.auth.backend import is_admin;
+  from desktop.auth.backend import is_admin
   from desktop.conf import APP_SWITCHER_ALTUS_BASE_URL, APP_SWITCHER_MOW_BASE_URL, DISPLAY_APP_SWITCHER, IS_K8S_ONLY, IS_MULTICLUSTER_ONLY, USE_DEFAULT_CONFIGURATION, USE_NEW_SIDE_PANELS, VCS
   from desktop.models import hue_version, _get_apps, get_cluster_config
 
-  from beeswax.conf import LIST_PARTITIONS_LIMIT
+  from beeswax.conf import DOWNLOAD_BYTES_LIMIT, DOWNLOAD_ROW_LIMIT, LIST_PARTITIONS_LIMIT
   from dashboard.conf import HAS_SQL_ENABLED
   from filebrowser.conf import SHOW_UPLOAD_BUTTON
   from indexer.conf import ENABLE_NEW_INDEXER
   from metadata.conf import has_catalog, has_readonly_catalog, has_optimizer, has_workload_analytics, OPTIMIZER, get_optimizer_url, get_catalog_url
   from metastore.conf import ENABLE_NEW_CREATE_TABLE
   from metastore.views import has_write_access
-  from notebook.conf import ENABLE_NOTEBOOK_2, ENABLE_QUERY_ANALYSIS, ENABLE_QUERY_BUILDER, ENABLE_QUERY_SCHEDULING, get_ordered_interpreters, SHOW_NOTEBOOKS
+  from notebook.conf import ENABLE_NOTEBOOK_2, ENABLE_QUERY_ANALYSIS, ENABLE_QUERY_BUILDER, ENABLE_QUERY_SCHEDULING, ENABLE_SQL_INDEXER, get_ordered_interpreters, SHOW_NOTEBOOKS
 %>
 
 <%namespace name="sqlDocIndex" file="/sql_doc_index.mako" />
@@ -75,6 +75,9 @@
 
   window.DROPZONE_HOME_DIR = '${ user.get_home_directory() if not user.is_anonymous() else "" }';
 
+  window.DOWNLOAD_ROW_LIMIT = ${ DOWNLOAD_ROW_LIMIT.get() if hasattr(DOWNLOAD_ROW_LIMIT, 'get') and DOWNLOAD_ROW_LIMIT.get() >= 0 else 'undefined' };
+  window.DOWNLOAD_BYTES_LIMIT = ${ DOWNLOAD_BYTES_LIMIT.get() if hasattr(DOWNLOAD_BYTES_LIMIT, 'get') and DOWNLOAD_BYTES_LIMIT.get() >= 0 else 'undefined' };
+
   window.USE_DEFAULT_CONFIGURATION = '${ USE_DEFAULT_CONFIGURATION.get() }' === 'True';
 
   window.USER_HAS_METADATA_WRITE_PERM = '${ user.has_hue_permission(action="write", app="metadata") }' === 'True';
@@ -82,6 +85,7 @@
   window.ENABLE_DOWNLOAD = '${ conf.ENABLE_DOWNLOAD.get() }' === 'True';
   window.ENABLE_NEW_CREATE_TABLE = '${ hasattr(ENABLE_NEW_CREATE_TABLE, 'get') and ENABLE_NEW_CREATE_TABLE.get()}' === 'True';
   window.ENABLE_NOTEBOOK_2 = '${ ENABLE_NOTEBOOK_2.get() }' === 'True';
+  window.ENABLE_SQL_INDEXER = '${ ENABLE_SQL_INDEXER.get() }' === 'True';
 
   window.ENABLE_QUERY_BUILDER = '${ ENABLE_QUERY_BUILDER.get() }' === 'True';
   window.ENABLE_QUERY_SCHEDULING = '${ ENABLE_QUERY_SCHEDULING.get() }' === 'True';

+ 1 - 1
desktop/libs/notebook/src/notebook/templates/editor_components2.mako

@@ -546,7 +546,7 @@
           <!-- /ko -->
         </ul>
 
-        <div class="tab-content" style="border: none; overflow-x: hidden">
+        <div class="tab-content" style="border: none; overflow-x: hidden; min-height: 250px;">
           <div class="tab-pane" id="queryHistory" style="min-height: 80px;" data-bind="css: {'active': currentQueryTab() == 'queryHistory'}, style: { 'height' : $parent.historyInitialHeight() > 0 ? Math.max($parent.historyInitialHeight(), 40) + 'px' : '' }">
             <!-- ko if: $parent.loadingHistory -->
             <div class="margin-top-10 margin-left-10">