Răsfoiți Sursa

HUE-9101 [editor] Create a separate component for the saved queries tab in editor v2

Johan Ahlen 6 ani în urmă
părinte
comite
4c299a7820

+ 20 - 0
desktop/core/src/desktop/js/apps/notebook2/components/__snapshots__/ko.savedQueries.test.js.snap

@@ -0,0 +1,20 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ko.savedQueries.js should render component 1`] = `
+"<div data-bind=\\"descendantsComplete: descendantsComplete, component: { name: &quot;saved-queries&quot;, params: params }\\"><div class=\\"snippet-tab-actions\\">
+  <form autocomplete=\\"off\\" class=\\"inline-block\\">
+    <input class=\\"input-small search-input\\" type=\\"text\\" autocorrect=\\"off\\" autocomplete=\\"do-not-autocomplete\\" autocapitalize=\\"off\\" spellcheck=\\"false\\" placeholder=\\"Search...\\" data-bind=\\"
+        textInput: filter,
+        clearable: filter
+      \\">
+  </form>
+</div><div class=\\"snippet-tab-body\\">
+  <!-- ko if: loading -->
+    <div>
+      <h1 class=\\"empty\\"><i class=\\"fa fa-spinner fa-spin\\"></i> Loading...</h1>
+    </div>
+  <!-- /ko -->
+
+  <!-- ko ifnot: loading --><!-- /ko -->
+</div></div>"
+`;

+ 190 - 0
desktop/core/src/desktop/js/apps/notebook2/components/ko.savedQueries.js

@@ -0,0 +1,190 @@
+// 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 * as ko from 'knockout';
+
+import componentUtils from 'ko/components/componentUtils';
+import DisposableComponent from 'ko/components/DisposableComponent';
+import I18n from 'utils/i18n';
+import { NAME as PAGINATOR_COMPONENT } from './ko.paginator';
+import apiHelper from 'api/apiHelper';
+
+export const UPDATE_SAVED_QUERIES_EVENT = 'update.saved.queries';
+export const NAME = 'saved-queries';
+
+// prettier-ignore
+const TEMPLATE = `
+<div class="snippet-tab-actions">
+  <form autocomplete="off" class="inline-block">
+    <input class="input-small search-input" type="text" ${ window.PREVENT_AUTOFILL_INPUT_ATTRS } placeholder="${ I18n('Search...') }" data-bind="
+        textInput: filter,
+        clearable: filter
+      "/>
+  </form>
+</div>
+
+<div class="snippet-tab-body">
+  <!-- ko if: loading -->
+    <div>
+      <h1 class="empty"><i class="fa fa-spinner fa-spin"></i> ${ I18n('Loading...') }</h1>
+    </div>
+  <!-- /ko -->
+
+  <!-- ko ifnot: loading -->
+    <!-- ko if: hasErrors -->
+      <div class="margin-top-10 margin-left-10" style="font-style: italic">${ I18n("Error loading the queries.") }</div>
+    <!-- /ko -->
+    <!-- ko if: !hasErrors() && !loading() && queries().length === 0 && filter() === '' -->
+      <div class="margin-top-10 margin-left-10" style="font-style: italic">${ I18n("You don't have any saved queries.") }</div>
+    <!-- /ko -->
+    <!-- ko if: !hasErrors() && !loading() && queries().length === 0 && filter() !== '' -->
+      <div class="margin-top-10 margin-left-10" style="font-style: italic">${ I18n('No queries found for') } <strong data-bind="text: filter"></strong>.</div>
+    <!-- /ko -->
+    <!-- ko if: !hasErrors() && !loading() && queries().length > 0 -->
+      <table class="table table-condensed margin-top-10 history-table">
+        <thead>
+        <tr>
+          <th style="width: 16%">${ I18n("Name") }</th>
+          <th style="width: 50%">${ I18n("Description") }</th>
+          <th style="width: 18%">${ I18n("Owner") }</th>
+          <th style="width: 16%">${ I18n("Last Modified") }</th>
+        </tr>
+        </thead>
+        <tbody data-bind="foreach: queries">
+        <tr data-bind="
+            click: function() {
+              $parent.openNotebook(uuid);
+            },
+            css: {
+              'highlight': uuid == $parent.currentNotebook.uuid(),
+              'pointer': uuid != $parent.currentNotebook.uuid()
+            }">
+          <td style="width: 16%"><span data-bind="ellipsis: { data: name, length: 50 }"></span></td>
+          <td style="width: 50%; white-space: normal"><span data-bind="text: description"></span></td>
+          <td style="width: 18%"><span data-bind="text: owner"></span></td>
+          <td style="width: 16%"><span data-bind="text: localeFormat(lastModified())"></span></td>
+        </tr>
+        </tbody>
+      </table>
+    <!-- /ko -->
+
+    <!-- ko component: {
+      name: '${ PAGINATOR_COMPONENT }',
+      params: {
+        currentPage: currentPage,
+        totalPages: totalPages,
+        onPageChange: fetchQueries.bind($data)
+      }
+    } --><!-- /ko -->
+  <!-- /ko -->
+</div>
+`;
+
+const QUERIES_PER_PAGE = 50;
+
+const adaptRawQuery = rawQuery => ({
+  uuid: rawQuery.uuid,
+  name: ko.observable(rawQuery.name),
+  owner: rawQuery.owner,
+  description: ko.observable(rawQuery.description),
+  lastModified: ko.observable(rawQuery.last_modified)
+});
+
+class SavedQueries extends DisposableComponent {
+  constructor(params) {
+    super();
+
+    this.currentNotebook = params.currentNotebook;
+    this.openFunction = params.openFunction;
+    this.type = params.type;
+    this.currentTab = params.currentTab;
+
+    this.loading = ko.observable(true);
+    this.hasErrors = ko.observable(false);
+    this.queries = ko.observableArray();
+
+    this.currentPage = ko.observable(1);
+    this.totalPages = ko.observable(1);
+
+    this.lastFetchQueriesRequest = undefined;
+
+    this.filter = ko.observable().extend({ rateLimit: 900 });
+    this.filter.subscribe(() => {
+      this.fetchQueries();
+    });
+
+    this.subscribe(UPDATE_SAVED_QUERIES_EVENT, details => {
+      if (!details.save_as) {
+        this.queries().some(item => {
+          if (item.uuid === details.uuid) {
+            item.name(details.name);
+            item.description(details.description);
+            item.lastModified(details.last_modified);
+            return true;
+          }
+        });
+      } else if (this.queries().length) {
+        this.queries.unshift(adaptRawQuery(details));
+      } else {
+        this.fetchQueries();
+      }
+    });
+
+    if (this.currentTab() === 'savedQueries') {
+      this.fetchQueries();
+    } else {
+      const sub = this.subscribe(this.currentTab, tab => {
+        if (tab === 'savedQueries') {
+          this.fetchQueries();
+          sub.dispose();
+        }
+      });
+    }
+  }
+
+  async fetchQueries() {
+    apiHelper.cancelActiveRequest(this.lastFetchQueriesRequest);
+
+    this.loading(true);
+    this.hasErrors(false);
+
+    this.lastFetchQueriesRequest = apiHelper.searchDocuments({
+      successCallback: foundDocuments => {
+        this.totalPages(Math.ceil(foundDocuments.count / QUERIES_PER_PAGE));
+        this.queries(foundDocuments.documents.map(adaptRawQuery));
+        this.loading(false);
+        this.hasErrors(false);
+      },
+      errorCallback: () => {
+        this.loading(false);
+        this.hasErrors(true);
+      },
+      page: this.currentPage(),
+      limit: QUERIES_PER_PAGE,
+      type: 'query-' + this.type(),
+      query: this.filter(),
+      include_trashed: false
+    });
+  }
+
+  openNotebook(uuid) {
+    if (window.getSelection().toString() === '' && uuid !== this.currentNotebook.uuid()) {
+      this.openFunction(uuid);
+    }
+  }
+}
+
+componentUtils.registerComponent(NAME, SavedQueries, TEMPLATE);

+ 14 - 0
desktop/core/src/desktop/js/apps/notebook2/components/ko.savedQueries.test.js

@@ -0,0 +1,14 @@
+import { koSetup } from 'jest/koTestUtils';
+import { NAME } from './ko.savedQueries';
+
+describe('ko.savedQueries.js', () => {
+  const setup = koSetup();
+
+  it('should render component', async () => {
+    const element = await setup.renderComponent(NAME, {
+      currentTab: () => undefined
+    });
+
+    expect(element.innerHTML).toMatchSnapshot();
+  });
+});

+ 0 - 11
desktop/core/src/desktop/js/apps/notebook2/editorViewModel.js

@@ -300,19 +300,11 @@ class EditorViewModel {
   }
 
   loadNotebook(notebookRaw, queryTab) {
-    let currentQueries;
-    if (this.selectedNotebook() != null) {
-      currentQueries = this.selectedNotebook().unload();
-    }
-
     const notebook = new Notebook(this, notebookRaw);
 
     if (notebook.snippets().length > 0) {
       huePubSub.publish('detach.scrolls', notebook.snippets()[0]);
       notebook.selectedSnippet(notebook.snippets()[notebook.snippets().length - 1].type());
-      if (currentQueries != null) {
-        notebook.snippets()[0].queries(currentQueries);
-      }
       notebook.snippets().forEach(snippet => {
         snippet.aceAutoExpand = false;
         snippet.statement_raw.valueHasMutated();
@@ -345,9 +337,6 @@ class EditorViewModel {
 
       if (notebook.isSaved()) {
         notebook.snippets()[0].currentQueryTab('savedQueries');
-        if (notebook.snippets()[0].queries().length === 0) {
-          notebook.snippets()[0].fetchQueries(); // Subscribe not updating yet
-        }
       }
     }
 

+ 2 - 13
desktop/core/src/desktop/js/apps/notebook2/notebook.js

@@ -26,6 +26,7 @@ import sessionManager from 'apps/notebook2/execution/sessionManager';
 
 import Snippet, { STATUS as SNIPPET_STATUS } from 'apps/notebook2/snippet';
 import { HISTORY_CLEARED_EVENT } from 'apps/notebook2/components/ko.queryHistory';
+import { UPDATE_SAVED_QUERIES_EVENT } from 'apps/notebook2/components/ko.savedQueries';
 
 export default class Notebook {
   constructor(vm, notebookRaw) {
@@ -348,19 +349,7 @@ export default class Notebook {
         this.isHistory(false);
         $(document).trigger('info', data.message);
         if (editorMode) {
-          if (!data.save_as) {
-            const existingQuery = this.snippets()[0]
-              .queries()
-              .filter(item => item.uuid() === data.uuid);
-            if (existingQuery.length > 0) {
-              existingQuery[0].name(data.name);
-              existingQuery[0].description(data.description);
-              existingQuery[0].last_modified(data.last_modified);
-            }
-          } else if (this.snippets()[0].queries().length > 0) {
-            // Saved queries tab already loaded
-            this.snippets()[0].queries.unshift(komapping.fromJS(data));
-          }
+          huePubSub.publish(UPDATE_SAVED_QUERIES_EVENT, data);
 
           if (this.coordinatorUuid() && this.schedulerViewModel) {
             this.saveScheduler();

+ 1 - 82
desktop/core/src/desktop/js/apps/notebook2/snippet.js

@@ -23,6 +23,7 @@ import 'apps/notebook2/components/ko.executableActions';
 import 'apps/notebook2/components/ko.executableLogs';
 import 'apps/notebook2/components/ko.executableProgressBar';
 import 'apps/notebook2/components/ko.snippetEditorActions';
+import 'apps/notebook2/components/ko.savedQueries';
 import 'apps/notebook2/components/ko.snippetResults';
 import 'apps/notebook2/components/ko.queryHistory';
 
@@ -279,48 +280,6 @@ export default class Snippet {
     this.currentQueryTab = ko.observable(snippetRaw.currentQueryTab || 'queryHistory');
     this.pinnedContextTabs = ko.observableArray(snippetRaw.pinnedContextTabs || []);
 
-    this.errorLoadingQueries = ko.observable(false);
-    this.loadingQueries = ko.observable(false);
-
-    this.queriesHasErrors = ko.observable(false);
-    this.queriesCurrentPage = ko.observable(
-      this.parentVm.selectedNotebook() && this.parentVm.selectedNotebook().snippets().length > 0
-        ? this.parentVm
-            .selectedNotebook()
-            .snippets()[0]
-            .queriesCurrentPage()
-        : 1
-    );
-    this.queriesTotalPages = ko.observable(
-      this.parentVm.selectedNotebook() && this.parentVm.selectedNotebook().snippets().length > 0
-        ? this.parentVm
-            .selectedNotebook()
-            .snippets()[0]
-            .queriesTotalPages()
-        : 1
-    );
-    this.queries = ko.observableArray([]);
-
-    this.queriesFilter = ko.observable('');
-    this.queriesFilterVisible = ko.observable(false);
-    this.queriesFilter.extend({ rateLimit: { method: 'notifyWhenChangesStop', timeout: 900 } });
-    this.queriesFilter.subscribe(() => {
-      this.fetchQueries();
-    });
-
-    this.lastFetchQueriesRequest = null;
-
-    this.lastQueriesPage = 1;
-    this.currentQueryTab.subscribe(newValue => {
-      huePubSub.publish(CURRENT_QUERY_TAB_SWITCHED_EVENT, newValue);
-      if (
-        newValue === 'savedQueries' &&
-        (this.queries().length === 0 || this.lastQueriesPage !== this.queriesCurrentPage())
-      ) {
-        this.fetchQueries();
-      }
-    });
-
     huePubSub.subscribeOnce(
       'assist.source.set',
       source => {
@@ -1171,32 +1130,6 @@ export default class Snippet {
     }
   }
 
-  fetchQueries() {
-    apiHelper.cancelActiveRequest(this.lastFetchQueriesRequest);
-
-    const QUERIES_PER_PAGE = 50;
-    this.lastQueriesPage = this.queriesCurrentPage();
-    this.loadingQueries(true);
-    this.queriesHasErrors(false);
-    this.lastFetchQueriesRequest = apiHelper.searchDocuments({
-      successCallback: foundDocuments => {
-        this.queriesTotalPages(Math.ceil(foundDocuments.count / QUERIES_PER_PAGE));
-        this.queries(komapping.fromJS(foundDocuments.documents)());
-        this.loadingQueries(false);
-        this.queriesHasErrors(false);
-      },
-      errorCallback: () => {
-        this.loadingQueries(false);
-        this.queriesHasErrors(true);
-      },
-      page: this.queriesCurrentPage(),
-      limit: QUERIES_PER_PAGE,
-      type: 'query-' + this.type(),
-      query: this.queriesFilter(),
-      include_trashed: false
-    });
-  }
-
   async getExternalStatement() {
     this.externalStatementLoaded(false);
     apiHelper
@@ -1396,13 +1329,6 @@ export default class Snippet {
     }
   }
 
-  nextQueriesPage() {
-    if (this.queriesCurrentPage() !== this.queriesTotalPages()) {
-      this.queriesCurrentPage(this.queriesCurrentPage() + 1);
-      this.fetchQueries();
-    }
-  }
-
   onKeydownInVariable(context, e) {
     if ((e.ctrlKey || e.metaKey) && e.which === 13) {
       // Ctrl-enter
@@ -1415,13 +1341,6 @@ export default class Snippet {
     return true;
   }
 
-  prevQueriesPage() {
-    if (this.queriesCurrentPage() !== 1) {
-      this.queriesCurrentPage(this.queriesCurrentPage() - 1);
-      this.fetchQueries();
-    }
-  }
-
   async queryCompatibility(targetPlatform) {
     apiHelper.cancelActiveRequest(this.lastCompatibilityRequest);
 

+ 4 - 1
desktop/core/src/desktop/js/ko/components/DisposableComponent.js

@@ -27,11 +27,14 @@ export default class DisposableComponent {
       this.disposals.push(() => {
         pubSub.remove();
       });
-    } else if (subscribable.subscribe) {
+      return pubSub;
+    }
+    if (subscribable.subscribe) {
       const sub = subscribable.subscribe(callback);
       this.disposals.push(() => {
         sub.dispose();
       });
+      return sub;
     }
   }
 

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

@@ -488,13 +488,11 @@
     <div class="query-history-container" data-bind="onComplete: function(){ redrawFixedHeaders(200); }">
       <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'}">
+          <li data-bind="click: function() { currentQueryTab('queryHistory'); }, css: { 'active': currentQueryTab() == 'queryHistory' }">
             <a class="inactive-action" style="display:inline-block" href="#queryHistory" data-toggle="tab">${_('Query History')}</a>
           </li>
-          <li class="margin-right-20" data-bind="click: function(){ currentQueryTab('savedQueries'); }, css: {'active': currentQueryTab() == 'savedQueries'}, onClickOutside: function () { if (queriesFilterVisible() && queriesFilter() === '') { queriesFilterVisible(false) } }">
+          <li class="margin-right-20" data-bind="click: function(){ currentQueryTab('savedQueries'); }, css: { 'active': currentQueryTab() == 'savedQueries' }">
             <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 -->
@@ -528,7 +526,7 @@
         </ul>
 
         <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'}">
+          <div class="tab-pane" id="queryHistory" style="min-height: 80px;" data-bind="css: { 'active': currentQueryTab() === 'queryHistory' }">
             <!-- ko component: {
               name: 'query-history',
               params: {
@@ -539,52 +537,20 @@
             } --><!-- /ko -->
           </div>
 
-          <div class="tab-pane" id="savedQueries" data-bind="css: {'active': currentQueryTab() == 'savedQueries'}" style="overflow: hidden">
-            <!-- ko if: loadingQueries -->
-            <div class="margin-top-10 margin-left-10">
-              <i class="fa fa-spinner fa-spin muted"></i>
-            </div>
-            <!-- /ko -->
-            <!-- ko if: queriesHasErrors() -->
-            <div class="margin-top-10 margin-left-10" style="font-style: italic">${ _("Error loading my queries") }</div>
-            <!-- /ko -->
-            <!-- ko if: !queriesHasErrors() && !loadingQueries() && queries().length === 0 && queriesFilter() === '' -->
-            <div class="margin-top-10 margin-left-10" style="font-style: italic">${ _("You don't have any saved query.") }</div>
-            <!-- /ko -->
-            <!-- ko if: !queriesHasErrors() && !loadingQueries() && queries().length === 0 && queriesFilter() !== '' -->
-            <div class="margin-top-10 margin-left-10" style="font-style: italic">${ _('No queries found for') } <strong data-bind="text: queriesFilter"></strong>.</div>
-            <!-- /ko -->
-            <!-- ko if: !queriesHasErrors() && !loadingQueries() && queries().length > 0 -->
-            <table class="table table-condensed margin-top-10 history-table">
-              <thead>
-              <tr>
-                <th style="width: 16%">${ _("Name") }</th>
-                <th style="width: 50%">${ _("Description") }</th>
-                <th style="width: 18%">${ _("Owner") }</th>
-                <th style="width: 16%">${ _("Last Modified") }</th>
-              </tr>
-              </thead>
-              <tbody data-bind="foreach: queries">
-              <tr data-bind="click: function() { if (uuid() != $root.selectedNotebook().uuid()) { $root.openNotebook(uuid(), 'savedQueries'); } }, css: { 'highlight': uuid() == $root.selectedNotebook().uuid(), 'pointer': uuid() != $root.selectedNotebook().uuid() }">
-                <td style="width: 16%"><span data-bind="ellipsis: {data: name(), length: 50}"></span></td>
-                <td style="width: 50%; white-space: normal"><span data-bind="text: description"></span></td>
-                <td style="width: 18%"><span data-bind="text: owner"></span></td>
-                <td style="width: 16%"><span data-bind="text: localeFormat(last_modified())"></span></td>
-              </tr>
-              </tbody>
-            </table>
-            <!-- /ko -->
-            <div class="pagination" data-bind="visible: queriesTotalPages() > 1">
-              <ul>
-                <li data-bind="css: { 'disabled' : queriesCurrentPage() === 1 }"><a href="javascript: void(0);" data-bind="click: prevQueriesPage">${ _("Prev") }</a></li>
-                <li class="active"><span data-bind="text: queriesCurrentPage() + '/' + queriesTotalPages()"></span></li>
-                <li data-bind="css: { 'disabled' : queriesCurrentPage() === queriesTotalPages() }"><a href="javascript: void(0);" data-bind="click: nextQueriesPage">${ _("Next") }</a></li>
-              </ul>
-            </div>
+          <div class="tab-pane" id="savedQueries" data-bind="css: { 'active': currentQueryTab() === 'savedQueries' }" style="overflow: hidden">
+            <!-- ko component: {
+              name: 'saved-queries',
+              params: {
+                currentNotebook: parentNotebook,
+                openFunction: parentVm.openNotebook.bind(parentVm),
+                type: type,
+                currentTab: currentQueryTab
+              }
+            } --><!-- /ko -->
           </div>
 
           % if ENABLE_QUERY_BUILDER.get():
-            <div class="tab-pane margin-top-10" id="queryBuilderTab" data-bind="css: {'active': currentQueryTab() == 'queryBuilderTab'}">
+            <div class="tab-pane margin-top-10" id="queryBuilderTab" data-bind="css: { 'active': currentQueryTab() === 'queryBuilderTab' }">
               <div id="queryBuilderAlert" style="display: none" class="alert">${ _('There are currently no rules defined. To get started, right click on any table column in the SQL Assist panel.') }</div>
               <table id="queryBuilder" class="table table-condensed">
                 <thead>