瀏覽代碼

HUE-8988 [editor] Add export and import to query history tab

The import and export actions are available in a right-click context menu on the query history tab.
Johan Ahlen 6 年之前
父節點
當前提交
b75cb744eb

+ 5 - 3
desktop/core/src/desktop/api2.py

@@ -656,12 +656,14 @@ def export_documents(request):
   else:
     selection = json.loads(request.POST.get('documents'))
 
+  include_history = request.GET.get('history', 'false') == 'true'
+
   # Only export documents the user has permissions to read
   docs = Document2.objects.documents(user=request.user, perms='both', include_history=True, include_trashed=True).\
     filter(id__in=selection).order_by('-id')
 
   # Add any dependencies to the set of exported documents
-  export_doc_set = _get_dependencies(docs)
+  export_doc_set = _get_dependencies(docs, include_history=include_history)
 
   # For directories, add any children docs to the set of exported documents
   export_doc_set.update(_get_dependencies(docs, deps_mode=False))
@@ -890,7 +892,7 @@ def _is_import_valid(documents):
     all(all(k in d['fields'] for k in ('uuid', 'owner')) for d in documents)
 
 
-def _get_dependencies(documents, deps_mode=True):
+def _get_dependencies(documents, deps_mode=True, include_history=False):
   """
   Given a list of Document2 objects, perform a depth-first search and return a set of documents with all
    dependencies (excluding history docs) included
@@ -903,7 +905,7 @@ def _get_dependencies(documents, deps_mode=True):
     stack = [doc]
     while stack:
       curr_doc = stack.pop()
-      if curr_doc not in doc_set and not curr_doc.is_history:
+      if curr_doc not in doc_set and (include_history or not curr_doc.is_history):
         doc_set.add(curr_doc)
         if deps_mode:
           deps_set = set(curr_doc.dependencies.all())

+ 20 - 0
desktop/core/src/desktop/js/api/apiHelper.js

@@ -1984,6 +1984,26 @@ class ApiHelper {
     return this.simplePost('/notebook/api/notebook/save', data);
   }
 
+  async getHistory(options) {
+    return new Promise((resolve, reject) => {
+      $.get('/notebook/api/get_history', {
+        doc_type: options.type,
+        limit: options.limit || 50,
+        page: options.page || 1,
+        doc_text: options.docFilter,
+        is_notification_manager: options.isNotificationManager
+      })
+        .done(data => {
+          if (this.successResponseIsError(data)) {
+            reject(self.assistErrorCallback(options)(data));
+            return;
+          }
+          resolve(data);
+        })
+        .fail(reject);
+    });
+  }
+
   /**
    *
    * @param {ExecutableStatement} executable

+ 4 - 0
desktop/core/src/desktop/js/apps/notebook/notebook.js

@@ -177,6 +177,9 @@ class Notebook {
         ? vm.selectedNotebook().history()
         : []
     );
+
+    // This is to keep the "Saved Query" tab selected when opening a doc from the left assist
+    // TODO: Refactor code to reflect purpose
     self.history.subscribe(val => {
       if (
         self.id() == null &&
@@ -187,6 +190,7 @@ class Notebook {
         self.snippets()[0].currentQueryTab('savedQueries');
       }
     });
+
     self.historyFilter = ko.observable('');
     self.historyFilterVisible = ko.observable(false);
     self.historyFilter.extend({ rateLimit: { method: 'notifyWhenChangesStop', timeout: 900 } });

+ 14 - 0
desktop/core/src/desktop/js/apps/notebook/snippet.js

@@ -2695,6 +2695,8 @@ class Snippet {
       }
       return true;
     };
+
+    self.refreshHistory = notebook.fetchHistory;
   }
 
   renderMarkdown() {
@@ -2703,6 +2705,18 @@ class Snippet {
     });
   }
 
+  async exportHistory() {
+    const historyResponse = await apiHelper.getHistory({ type: this.type(), limit: 500 });
+
+    if (historyResponse && historyResponse.history) {
+      window.location.href =
+        window.HUE_BASE_URL +
+        '/desktop/api2/doc/export?history=true&documents=[' +
+        historyResponse.history.map(historyDoc => historyDoc.id).join(',') +
+        ']';
+    }
+  }
+
   toggleAllResultColumns(linkElement) {
     const $t = $(linkElement)
       .parents('.snippet')

+ 5 - 1
desktop/core/src/desktop/js/apps/notebook2/notebook.js

@@ -111,16 +111,20 @@ class Notebook {
         ? vm.selectedNotebook().history()
         : []
     );
+
+    // This is to keep the "Saved Query" tab selected when opening a doc from the left assist
+    // TODO: Refactor code to reflect purpose
     self.history.subscribe(val => {
       if (
         self.id() == null &&
-        val.length === 0 &&
+        val.length == 0 &&
         self.historyFilter() === '' &&
         !vm.isNotificationManager()
       ) {
         self.snippets()[0].currentQueryTab('savedQueries');
       }
     });
+
     self.historyFilter = ko.observable('');
     self.historyFilterVisible = ko.observable(false);
     self.historyFilter.extend({ rateLimit: { method: 'notifyWhenChangesStop', timeout: 900 } });

+ 14 - 0
desktop/core/src/desktop/js/apps/notebook2/snippet.js

@@ -1125,6 +1125,8 @@ class Snippet {
         self.progress(executable.progress);
       }
     });
+
+    self.refreshHistory = notebook.fetchHistory;
   }
 
   ace(newVal) {
@@ -1438,6 +1440,18 @@ class Snippet {
     );
   }
 
+  async exportHistory() {
+    const historyResponse = await apiHelper.getHistory({ type: this.type(), limit: 500 });
+
+    if (historyResponse && historyResponse.history) {
+      window.location.href =
+        window.HUE_BASE_URL +
+        '/desktop/api2/doc/export?history=true&documents=[' +
+        historyResponse.history.map(historyDoc => historyDoc.id).join(',') +
+        ']';
+    }
+  }
+
   fetchExecutionAnalysis() {
     const self = this;
     if (self.type() === TYPE.impala) {

+ 30 - 14
desktop/libs/notebook/src/notebook/templates/editor_components.mako

@@ -614,31 +614,47 @@ ${ sqlSyntaxDropdown.sqlSyntaxDropdown() }
     <div data-bind="delayedOverflow: 'slow', css: resultsKlass" style="margin-top: 5px; position: relative;">
       <ul class="nav nav-tabs nav-tabs-editor">
         <li data-bind="click: function(){ currentQueryTab('queryHistory'); }, css: {'active': currentQueryTab() == 'queryHistory'}, onClickOutside: function () { if ($parent.historyFilterVisible() && $parent.historyFilter() === '') { $parent.historyFilterVisible(false) } }">
-          <a class="inactive-action" href="#queryHistory" data-toggle="tab">${_('Query History')}
-            <div class="inline-block inactive-action margin-left-10 pointer visible-on-hover" title="${_('Search the query history')}" data-bind="click: function(data, e){ $parent.historyFilterVisible(!$parent.historyFilterVisible()); if ($parent.historyFilterVisible()) { window.setTimeout(function(){ $(e.target).parent().siblings('input').focus(); }, 0); } else { $parent.historyFilter('') }}"><i class="snippet-icon fa fa-search"></i></div>
-            <input class="input-small inline-tab-filter" type="text" data-bind="visible: $parent.historyFilterVisible, clearable: $parent.historyFilter, valueUpdate:'afterkeydown'" placeholder="${ _('Search...') }">
-            <div class="inline-block inactive-action pointer visible-on-hover" title="${_('Clear the query history')}" data-target="#clearHistoryModal${ suffix }" data-toggle="modal" rel="tooltip" data-bind="visible: $parent.history().length > 0"><i class="snippet-icon fa fa-calendar-times-o"></i></div>
-          </a>
+          <a class="inactive-action" style="display:inline-block" href="#queryHistory" data-toggle="tab">${_('Query History')}</a>
+          <div style="margin-left: -15px;" class="inline-block inactive-action pointer visible-on-hover" title="${_('Search the query history')}" data-bind="click: function(data, e){ $parent.historyFilterVisible(!$parent.historyFilterVisible()); if ($parent.historyFilterVisible()) { window.setTimeout(function(){ $(e.target).parent().siblings('input').focus(); }, 0); } else { $parent.historyFilter('') }}"><i class="snippet-icon fa fa-search"></i></div>
+          <input class="input-small inline-tab-filter" type="text" data-bind="visible: $parent.historyFilterVisible, clearable: $parent.historyFilter, valueUpdate:'afterkeydown'" placeholder="${ _('Search...') }">
+          <div class="dropdown inline-block inactive-action pointer visible-on-hover">
+            <a class="" data-toggle="dropdown" href="javascript: void(0)">
+              <i class="fa fa-fw fa-ellipsis-v"></i>
+            </a>
+            <ul class="dropdown-menu">
+              <li data-bind="visible: $parent.history().length > 0">
+                <!-- ko if: editorMode -->
+                <a data-target="#clearHistoryModal${ suffix }" data-toggle="modal" rel="tooltip">
+                  <i class="fa fa-fw fa-calendar-times-o"></i> Clear
+                </a>
+                <!-- /ko -->
+                <!-- ko ifnot: editorMode --><!-- /ko -->
+              </li>
+              <li><a href="javascript:void(0);" data-bind="click: exportHistory.bind($data)"><i class="fa fa-fw fa-download"></i> ${_('Export')}</a></li>
+              <li><a href="javascript:void(0);" data-bind="publish: { 'show.import.documents.modal': { importedCallback: refreshHistory.bind($data), isHistory: true }}"><i class="fa fa-fw fa-upload"></i> ${_('Import' )}</a></li>
+            </ul>
+          </div>
         </li>
-        <li data-bind="click: function(){ currentQueryTab('savedQueries'); }, css: {'active': currentQueryTab() == 'savedQueries'}, onClickOutside: function () { if (queriesFilterVisible() && queriesFilter() === '') { queriesFilterVisible(false) } }">
-          <a class="inactive-action" href="#savedQueries" data-toggle="tab">${_('Saved Queries')}
-            <div class="inline-block inactive-action margin-left-10 pointer visible-on-hover" title="${_('Search the saved queries')}" data-bind="visible: !queriesHasErrors(), click: function(data, e){ queriesFilterVisible(!queriesFilterVisible()); if (queriesFilterVisible()) { window.setTimeout(function(){ $(e.target).parent().siblings('input').focus(); }, 0); } else { queriesFilter('') }}"><i class="snippet-icon fa fa-search"></i></div>
-            <input class="input-small inline-tab-filter" type="text" data-bind="visible: queriesFilterVisible, clearable: queriesFilter, valueUpdate:'afterkeydown'" placeholder="${ _('Search...') }">
-          </a>
+        <li class="margin-right-20" data-bind="click: function(){ currentQueryTab('savedQueries'); }, css: {'active': currentQueryTab() == 'savedQueries'}, onClickOutside: function () { if (queriesFilterVisible() && queriesFilter() === '') { queriesFilterVisible(false) } }">
+          <a class="inactive-action" style="display:inline-block" href="#savedQueries" data-toggle="tab">${_('Saved Queries')}</a>
+          <div style="margin-left: -15px;" class="inline-block inactive-action pointer visible-on-hover" title="${_('Search the saved queries')}" data-bind="visible: !queriesHasErrors(), click: function(data, e){ queriesFilterVisible(!queriesFilterVisible()); if (queriesFilterVisible()) { window.setTimeout(function(){ $(e.target).parent().siblings('input').focus(); }, 0); } else { queriesFilter('') }}"><i class="snippet-icon fa fa-search"></i></div>
+          <input class="input-small inline-tab-filter" type="text" data-bind="visible: queriesFilterVisible, clearable: queriesFilter, valueUpdate:'afterkeydown'" placeholder="${ _('Search...') }">
         </li>
         % if ENABLE_QUERY_BUILDER.get():
         <!-- ko if: isSqlDialect -->
-        <li data-bind="click: function(){ currentQueryTab('queryBuilderTab'); }, css: {'active': currentQueryTab() == 'queryBuilderTab'}"><a class="inactive-action" href="#queryBuilderTab" data-toggle="tab">${_('Query Builder')}</a></li>
+        <li style="margin-right: 25px;" data-bind="click: function(){ currentQueryTab('queryBuilderTab'); }, css: {'active': currentQueryTab() == 'queryBuilderTab'}"><a class="inactive-action" href="#queryBuilderTab" data-toggle="tab">${_('Query Builder')}</a></li>
         <!-- /ko -->
         % endif
         <!-- ko if: result.hasSomeResults -->
         <li data-bind="click: function(){ currentQueryTab('queryResults'); }, css: {'active': currentQueryTab() == 'queryResults'}">
-          <a class="inactive-action" href="#queryResults" data-toggle="tab">${_('Results')}
+          <a class="inactive-action" style="display:inline-block" href="#queryResults" data-toggle="tab">${_('Results')}
             <!-- ko if: result.rows() != null  -->
               (<span data-bind="text: result.rows().toLocaleString() + (type() == 'impala' && result.rows() == 1024 ? '+' : '')" title="${ _('Number of rows') }"></span>)
             <!-- /ko -->
+          </a>
+          <div style="margin-left: -15px;" class="inline-block">
             <!-- ko if: showGrid -->
-            <div class="inline-block inactive-action pointer margin-left-10 visible-on-hover" title="${_('Search the results')}" data-bind="click: function(data, e){ $(e.target).parents('.snippet').find('.resultTable').hueDataTable().fnShowSearch() }">
+            <div class="inline-block inactive-action pointer visible-on-hover" title="${_('Search the results')}" data-bind="click: function(data, e){ $(e.target).parents('.snippet').find('.resultTable').hueDataTable().fnShowSearch() }">
               <i class="snippet-icon fa fa-search"></i>
             </div>
             <!-- /ko -->
@@ -648,7 +664,7 @@ ${ sqlSyntaxDropdown.sqlSyntaxDropdown() }
             <div class="inline-block inactive-action pointer visible-on-hover" title="${_('Collapse results')}" rel="tooltip" data-bind="visible: $root.isResultFullScreenMode(), click: function(){ $root.isResultFullScreenMode(false); }">
               <i class="snippet-icon fa fa-compress"></i>
             </div>
-          </a>
+          </div>
         </li>
         <!-- /ko -->
         <!-- ko if: result.explanation().length > 0 -->

+ 30 - 14
desktop/libs/notebook/src/notebook/templates/editor_components2.mako

@@ -544,31 +544,47 @@
       <div data-bind="delayedOverflow: 'slow', css: resultsKlass" style="margin-top: 5px; position: relative;">
         <ul class="nav nav-tabs nav-tabs-editor">
           <li data-bind="click: function(){ currentQueryTab('queryHistory'); }, css: {'active': currentQueryTab() == 'queryHistory'}, onClickOutside: function () { if ($parent.historyFilterVisible() && $parent.historyFilter() === '') { $parent.historyFilterVisible(false) } }">
-            <a class="inactive-action" href="#queryHistory" data-toggle="tab">${_('Query History')}
-              <div class="inline-block inactive-action margin-left-10 pointer visible-on-hover" title="${_('Search the query history')}" data-bind="click: function(data, e){ $parent.historyFilterVisible(!$parent.historyFilterVisible()); if ($parent.historyFilterVisible()) { window.setTimeout(function(){ $(e.target).parent().siblings('input').focus(); }, 0); } else { $parent.historyFilter('') }}"><i class="snippet-icon fa fa-search"></i></div>
-              <input class="input-small inline-tab-filter" type="text" data-bind="visible: $parent.historyFilterVisible, clearable: $parent.historyFilter, valueUpdate:'afterkeydown'" placeholder="${ _('Search...') }">
-              <div class="inline-block inactive-action pointer visible-on-hover" title="${_('Clear the query history')}" data-target="#clearHistoryModal${ suffix }" data-toggle="modal" rel="tooltip" data-bind="visible: $parent.history().length > 0"><i class="snippet-icon fa fa-calendar-times-o"></i></div>
-            </a>
+            <a class="inactive-action" style="display:inline-block" href="#queryHistory" data-toggle="tab">${_('Query History')}</a>
+            <div style="margin-left: -15px;" class="inline-block inactive-action pointer visible-on-hover" title="${_('Search the query history')}" data-bind="click: function(data, e){ $parent.historyFilterVisible(!$parent.historyFilterVisible()); if ($parent.historyFilterVisible()) { window.setTimeout(function(){ $(e.target).parent().siblings('input').focus(); }, 0); } else { $parent.historyFilter('') }}"><i class="snippet-icon fa fa-search"></i></div>
+            <input class="input-small inline-tab-filter" type="text" data-bind="visible: $parent.historyFilterVisible, clearable: $parent.historyFilter, valueUpdate:'afterkeydown'" placeholder="${ _('Search...') }">
+            <div class="dropdown inline-block inactive-action pointer visible-on-hover">
+              <a class="" data-toggle="dropdown" href="javascript: void(0)">
+                <i class="fa fa-fw fa-ellipsis-v"></i>
+              </a>
+              <ul class="dropdown-menu">
+                <li data-bind="visible: $parent.history().length > 0">
+                  <!-- ko if: editorMode -->
+                  <a data-target="#clearHistoryModal${ suffix }" data-toggle="modal" rel="tooltip">
+                    <i class="fa fa-fw fa-calendar-times-o"></i> Clear
+                  </a>
+                  <!-- /ko -->
+                  <!-- ko ifnot: editorMode --><!-- /ko -->
+                </li>
+                <li><a href="javascript:void(0);" data-bind="click: exportHistory.bind($data)"><i class="fa fa-fw fa-download"></i> ${_('Export')}</a></li>
+                <li><a href="javascript:void(0);" data-bind="publish: { 'show.import.documents.modal': { importedCallback: refreshHistory.bind($data.parentNotebook), isHistory: true }}"><i class="fa fa-fw fa-upload"></i> ${_('Import' )}</a></li>
+              </ul>
+            </div>
           </li>
-          <li data-bind="click: function(){ currentQueryTab('savedQueries'); }, css: {'active': currentQueryTab() == 'savedQueries'}, onClickOutside: function () { if (queriesFilterVisible() && queriesFilter() === '') { queriesFilterVisible(false) } }">
-            <a class="inactive-action" href="#savedQueries" data-toggle="tab">${_('Saved Queries')}
-              <div class="inline-block inactive-action margin-left-10 pointer visible-on-hover" title="${_('Search the saved queries')}" data-bind="visible: !queriesHasErrors(), click: function(data, e){ queriesFilterVisible(!queriesFilterVisible()); if (queriesFilterVisible()) { window.setTimeout(function(){ $(e.target).parent().siblings('input').focus(); }, 0); } else { queriesFilter('') }}"><i class="snippet-icon fa fa-search"></i></div>
-              <input class="input-small inline-tab-filter" type="text" data-bind="visible: queriesFilterVisible, clearable: queriesFilter, valueUpdate:'afterkeydown'" placeholder="${ _('Search...') }">
-            </a>
+          <li class="margin-right-20" data-bind="click: function(){ currentQueryTab('savedQueries'); }, css: {'active': currentQueryTab() == 'savedQueries'}, onClickOutside: function () { if (queriesFilterVisible() && queriesFilter() === '') { queriesFilterVisible(false) } }">
+            <a class="inactive-action" style="display:inline-block" href="#savedQueries" data-toggle="tab">${_('Saved Queries')}</a>
+            <div style="margin-left: -15px;" class="inline-block inactive-action pointer visible-on-hover" title="${_('Search the saved queries')}" data-bind="visible: !queriesHasErrors(), click: function(data, e){ queriesFilterVisible(!queriesFilterVisible()); if (queriesFilterVisible()) { window.setTimeout(function(){ $(e.target).parent().siblings('input').focus(); }, 0); } else { queriesFilter('') }}"><i class="snippet-icon fa fa-search"></i></div>
+            <input class="input-small inline-tab-filter" type="text" data-bind="visible: queriesFilterVisible, clearable: queriesFilter, valueUpdate:'afterkeydown'" placeholder="${ _('Search...') }">
           </li>
           % if ENABLE_QUERY_BUILDER.get():
             <!-- ko if: isSqlDialect -->
-            <li data-bind="click: function(){ currentQueryTab('queryBuilderTab'); }, css: {'active': currentQueryTab() == 'queryBuilderTab'}"><a class="inactive-action" href="#queryBuilderTab" data-toggle="tab">${_('Query Builder')}</a></li>
+            <li style="margin-right: 25px;" data-bind="click: function(){ currentQueryTab('queryBuilderTab'); }, css: {'active': currentQueryTab() == 'queryBuilderTab'}"><a class="inactive-action" href="#queryBuilderTab" data-toggle="tab">${_('Query Builder')}</a></li>
             <!-- /ko -->
           % endif
           <!-- ko if: result.hasSomeResults -->
           <li data-bind="click: function(){ currentQueryTab('queryResults'); }, css: {'active': currentQueryTab() == 'queryResults'}">
-            <a class="inactive-action" href="#queryResults" data-toggle="tab">${_('Results')}
+            <a class="inactive-action" style="display:inline-block" href="#queryResults" data-toggle="tab">${_('Results')}
               <!-- ko if: result.rows() != null  -->
               (<span data-bind="text: result.rows().toLocaleString() + (type() == 'impala' && result.rows() == 1024 ? '+' : '')" title="${ _('Number of rows') }"></span>)
               <!-- /ko -->
+            </a>
+            <div style="margin-left: -15px;" class="inline-block">
               <!-- ko if: showGrid -->
-              <div class="inline-block inactive-action pointer margin-left-10 visible-on-hover" title="${_('Search the results')}" data-bind="click: function(data, e){ $(e.target).parents('.snippet').find('.resultTable').hueDataTable().fnShowSearch() }">
+              <div class="inline-block inactive-action pointer visible-on-hover" title="${_('Search the results')}" data-bind="click: function(data, e){ $(e.target).parents('.snippet').find('.resultTable').hueDataTable().fnShowSearch() }">
                 <i class="snippet-icon fa fa-search"></i>
               </div>
               <!-- /ko -->
@@ -578,7 +594,7 @@
               <div class="inline-block inactive-action pointer visible-on-hover" title="${_('Collapse results')}" rel="tooltip" data-bind="visible: $root.isResultFullScreenMode(), click: function(){ $root.isResultFullScreenMode(false); }">
                 <i class="snippet-icon fa fa-compress"></i>
               </div>
-            </a>
+            </div>
           </li>
           <!-- /ko -->
           <!-- ko if: result.explanation().length > 0 -->