فهرست منبع

HUE-9101 [editor] Extract the query history to a contained component for editor v2

Johan Ahlen 6 سال پیش
والد
کامیت
54d9a443fe

+ 0 - 31
desktop/core/src/desktop/js/apps/notebook2/app.js

@@ -517,37 +517,6 @@ export const initNotebook2 = () => {
           if (tab !== 'queryResults') {
             $('.hue-datatable-search').hide();
           }
-          if (tab === 'queryHistory') {
-            hueUtils.waitForRendered(
-              $('#queryHistory .history-table'),
-              el => {
-                return el.is(':visible');
-              },
-              () => {
-                viewModel.selectedNotebook().forceHistoryInitialHeight(true);
-                huePubSub.publish('editor.calculate.history.height');
-              }
-            );
-          }
-        },
-        HUE_PUB_SUB_EDITOR_ID
-      );
-
-      huePubSub.subscribe(
-        'editor.calculate.history.height',
-        () => {
-          if (
-            viewModel.editorMode() &&
-            (viewModel.selectedNotebook().historyInitialHeight() === 0 ||
-              viewModel.selectedNotebook().forceHistoryInitialHeight())
-          ) {
-            let h = $('#queryHistory .history-table').height();
-            if (h === 0) {
-              h = viewModel.selectedNotebook().history().length * 32;
-            }
-            viewModel.selectedNotebook().historyInitialHeight(h + 80); // add pagination too
-            viewModel.selectedNotebook().forceHistoryInitialHeight(false);
-          }
         },
         HUE_PUB_SUB_EDITOR_ID
       );

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

@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ko.queryHistory.js should render component 1`] = `
+"<div data-bind=\\"descendantsComplete: descendantsComplete, component: { name: &quot;query-history&quot;, params: params }\\"><div class=\\"margin-top-10 margin-left-10\\">
+    <i class=\\"fa fa-spinner fa-spin muted\\"></i>
+  </div><div class=\\"margin-top-10 margin-left-10\\" style=\\"font-style: italic\\">No queries to be shown.</div><div class=\\"margin-top-10 margin-left-10\\" style=\\"font-style: italic\\">No queries found for <strong data-bind=\\"text: historyFilter\\"></strong>.</div><table class=\\"table table-condensed margin-top-10 history-table\\">
+      <tbody data-bind=\\"foreach: history\\"></tbody>
+    </table><div class=\\"pagination\\" data-bind=\\"visible: historyTotalPages() > 1\\" style=\\"display: none;\\">
+    <ul>
+      <li data-bind=\\"css: { 'disabled' : historyCurrentPage() === 1 }\\" class=\\"disabled\\">
+        <a href=\\"javascript: void(0);\\" data-bind=\\"click: prevHistoryPage.bind($data)\\">Prev</a>
+      </li>
+      <li class=\\"active\\"><span data-bind=\\"text: historyCurrentPage() + '/' + historyTotalPages()\\">1/1</span></li>
+      <li data-bind=\\"css: { 'disabled' : historyCurrentPage() === historyTotalPages() }\\" class=\\"disabled\\">
+        <a href=\\"javascript: void(0);\\" data-bind=\\"click: nextHistoryPage.bind($data)\\">Next</a>
+      </li>
+    </ul>
+  </div></div>"
+`;

+ 267 - 0
desktop/core/src/desktop/js/apps/notebook2/components/ko.queryHistory.js

@@ -0,0 +1,267 @@
+// 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 apiHelper from 'api/apiHelper';
+import componentUtils from 'ko/components/componentUtils';
+import DisposableComponent from 'ko/components/DisposableComponent';
+import I18n from 'utils/i18n';
+import { EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
+import { sleep } from 'utils/hueUtils';
+
+export const NAME = 'query-history';
+
+// prettier-ignore
+const TEMPLATE = `
+<!-- ko if: loadingHistory -->
+  <div class="margin-top-10 margin-left-10">
+    <i class="fa fa-spinner fa-spin muted"></i>
+  </div>
+<!-- /ko -->
+
+<!-- ko ifnot: loadingHistory -->
+  <!-- ko if: !history().length -->
+    <!-- ko ifnot: historyFilter -->
+      <div class="margin-top-10 margin-left-10" style="font-style: italic">${ I18n("No queries to be shown.") }</div>
+    <!-- /ko -->
+    <!-- ko if: historyFilter -->
+      <div class="margin-top-10 margin-left-10" style="font-style: italic">${ I18n('No queries found for') } <strong data-bind="text: historyFilter"></strong>.</div>
+    <!-- /ko -->
+  <!-- /ko -->
+
+  <!-- ko if: history().length -->
+    <table class="table table-condensed margin-top-10 history-table">
+      <tbody data-bind="foreach: history">
+        <tr data-bind="
+            click: function() {
+              $parent.openNotebook(uuid);
+            },
+            css: {
+              'highlight': uuid === $parent.currentNotebook.uuid(),
+              'pointer': uuid !== $parent.currentNotebook.uuid()
+            }
+          ">
+          <td style="width: 100px" class="muted" data-bind="style: { 'border-top-width': $index() === 0 ? '0' : ''}">
+            <span data-bind="momentFromNow: { data: lastExecuted, interval: 10000, titleFormat: 'LLL' }"></span>
+          </td>
+          <td style="width: 25px" class="muted" data-bind="style: { 'border-top-width': $index() === 0 ? '0' : ''}">
+            <!-- ko switch: status -->
+              <!-- ko case: 'running' -->
+              <div class="history-status" data-bind="tooltip: { title: '${ I18n("Query running") }', placement: 'bottom' }"><i class="fa fa-fighter-jet fa-fw"></i></div>
+              <!-- /ko -->
+              <!-- ko case: 'failed' -->
+              <div class="history-status" data-bind="tooltip: { title: '${ I18n("Query failed") }', placement: 'bottom' }"><i class="fa fa-exclamation fa-fw"></i></div>
+              <!-- /ko -->
+              <!-- ko case: 'available' -->
+              <div class="history-status" data-bind="tooltip: { title: '${ I18n("Result available") }', placement: 'bottom' }"><i class="fa fa-check fa-fw"></i></div>
+              <!-- /ko -->
+              <!-- ko case: 'expired' -->
+              <div class="history-status" data-bind="tooltip: { title: '${ I18n("Result expired") }', placement: 'bottom' }"><i class="fa fa-unlink fa-fw"></i></div>
+              <!-- /ko -->
+            <!-- /ko -->
+          </td>
+          <td style="width: 25px" class="muted" data-bind="
+              ellipsis: {
+                data: name,
+                length: 30
+              },
+              style: {
+                'border-top-width': $index() === 0 ? '0' : ''
+              }
+            "></td>
+          <td data-bind="
+              style: {
+                'border-top-width': $index() === 0 ? '0' : ''
+              },
+              click: function() {
+                $parent.openNotebook(uuid)
+              },
+              clickBubble: false
+            "><div data-bind="highlight: { value: query, dialect: $parent.type }"></div></td>
+        </tr>
+      </tbody>
+    </table>
+  <!-- /ko -->
+
+  <div class="pagination" data-bind="visible: historyTotalPages() > 1" style="display: none;">
+    <ul>
+      <li data-bind="css: { 'disabled' : historyCurrentPage() === 1 }">
+        <a href="javascript: void(0);" data-bind="click: prevHistoryPage.bind($data)">${ I18n("Prev") }</a>
+      </li>
+      <li class="active"><span data-bind="text: historyCurrentPage() + '/' + historyTotalPages()"></span></li>
+      <li data-bind="css: { 'disabled' : historyCurrentPage() === historyTotalPages() }">
+        <a href="javascript: void(0);" data-bind="click: nextHistoryPage.bind($data)">${ I18n("Next") }</a>
+      </li>
+    </ul>
+  </div>
+<!-- /ko -->
+`;
+
+const QUERIES_PER_PAGE = 50;
+
+const AVAILABLE_INTERVAL = 60000 * 5;
+const STARTING_RUNNING_INTERVAL = 30000;
+
+const trimEllipsis = str => str.substring(0, 1000) + (str.length > 1000 ? '...' : '');
+
+class QueryHistory extends DisposableComponent {
+  constructor(params) {
+    super();
+    this.currentNotebook = params.currentNotebook;
+    this.type = params.type;
+    this.openFunction = params.openFunction;
+
+    this.loadingHistory = ko.observable(true);
+    this.history = ko.observableArray();
+    this.historyFilter = ko.observable('');
+
+    this.historyCurrentPage = ko.observable(1);
+    this.historyTotalPages = ko.observable(1);
+
+    this.refreshStatusFailed = false;
+
+    this.addDisposalCallback(() => {
+      this.refreshStatusFailed = true; // Cancels the status check intervals
+    });
+
+    this.subscribe(this.historyFilter, () => {
+      if (this.historyCurrentPage() !== 1) {
+        this.historyCurrentPage(1);
+      } else {
+        this.fetchHistory();
+      }
+    });
+
+    this.subscribe(this.historyCurrentPage, () => {
+      this.fetchHistory();
+    });
+
+    this.fetchHistory();
+  }
+
+  async fetchHistory() {
+    this.loadingHistory(true);
+
+    try {
+      const historyData = await apiHelper.getHistory({
+        type: this.type(),
+        limit: QUERIES_PER_PAGE,
+        page: this.historyCurrentPage(),
+        docFilter: this.historyFilter()
+      });
+
+      if (historyData && historyData.history) {
+        this.history(
+          historyData.history.map(historyRecord => ({
+            url: historyRecord.absoluteUrl,
+            query: trimEllipsis(historyRecord.data.statement),
+            lastExecuted: historyRecord.data.lastExecuted,
+            status: ko.observable(historyRecord.data.status),
+            name: historyRecord.name,
+            uuid: historyRecord.uuid
+          }))
+        );
+      } else {
+        this.history([]);
+      }
+
+      this.historyTotalPages(Math.ceil(historyData.count / QUERIES_PER_PAGE));
+    } catch (err) {
+      this.history([]);
+      this.historyTotalPages(1);
+    }
+
+    this.loadingHistory(false);
+    this.refreshStatus(
+      [EXECUTION_STATUS.starting, EXECUTION_STATUS.running],
+      STARTING_RUNNING_INTERVAL
+    );
+    this.refreshStatus([EXECUTION_STATUS.available], AVAILABLE_INTERVAL);
+  }
+
+  prevHistoryPage() {
+    if (this.historyCurrentPage() !== 1) {
+      this.historyCurrentPage(this.historyCurrentPage() - 1);
+    }
+  }
+
+  nextHistoryPage() {
+    if (this.historyCurrentPage() < this.historyTotalPages()) {
+      this.historyCurrentPage(this.historyCurrentPage() + 1);
+    }
+  }
+
+  openNotebook(uuid) {
+    if (window.getSelection().toString() === '' && uuid !== this.currentNotebook.uuid()) {
+      this.openFunction(uuid);
+    }
+  }
+
+  async refreshStatus(statusesToRefresh, interval) {
+    const statusIndex = {};
+    statusesToRefresh.forEach(status => {
+      statusIndex[status] = true;
+    });
+
+    const items = this.history()
+      .filter(item => statusIndex[item.status()])
+      .slice(0, 25);
+
+    const refreshStatusForItem = item => {
+      if (this.refreshStatusFailed) {
+        return;
+      }
+      apiHelper
+        .checkStatus({
+          notebookJson: JSON.stringify({ uuid: item.uuid }),
+          silenceErrors: true
+        })
+        .then(data => {
+          if (data.status === -3) {
+            item.status(EXECUTION_STATUS.expired);
+          } else if (data.status !== 0) {
+            item.status(EXECUTION_STATUS.failed);
+          } else if (data.query_status.status) {
+            item.status(data.query_status.status);
+          }
+        })
+        .fail(() => {
+          items.length = 0;
+          this.refreshStatusFailed = true;
+          console.warn('Failed checking status for the history items.');
+        })
+        .always(async () => {
+          if (items.length) {
+            await sleep(1000);
+            refreshStatusForItem(items.pop());
+          } else if (!this.refreshStatusFailed) {
+            await sleep(interval);
+            this.refreshStatus(statusesToRefresh, interval);
+          }
+        });
+    };
+
+    if (items.length) {
+      refreshStatusForItem(items.pop());
+    } else if (!this.refreshStatusFailed) {
+      await sleep(interval);
+      this.refreshStatus(statusesToRefresh, interval);
+    }
+  }
+}
+
+componentUtils.registerComponent(NAME, QueryHistory, TEMPLATE);

+ 12 - 0
desktop/core/src/desktop/js/apps/notebook2/components/ko.queryHistory.test.js

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

+ 0 - 138
desktop/core/src/desktop/js/apps/notebook2/notebook.js

@@ -93,25 +93,6 @@ export default class Notebook {
     this.historyFilter = ko.observable('');
     this.historyFilterVisible = ko.observable(false);
     this.historyFilter.extend({ rateLimit: { method: 'notifyWhenChangesStop', timeout: 900 } });
-    this.historyFilter.subscribe(() => {
-      if (this.historyCurrentPage() !== 1) {
-        this.historyCurrentPage(1);
-      } else {
-        this.fetchHistory();
-      }
-    });
-    this.loadingHistory = ko.observable(this.history().length === 0);
-    this.historyInitialHeight = ko.observable(0).extend({ throttle: 1000 });
-    this.forceHistoryInitialHeight = ko.observable(false);
-    this.historyCurrentPage = ko.observable(
-      vm.selectedNotebook() ? vm.selectedNotebook().historyCurrentPage() : 1
-    );
-    this.historyCurrentPage.subscribe(() => {
-      this.fetchHistory();
-    });
-    this.historyTotalPages = ko.observable(
-      vm.selectedNotebook() ? vm.selectedNotebook().historyTotalPages() : 1
-    );
 
     this.schedulerViewModel = null;
     this.schedulerViewModelIsLoaded = ko.observable(false);
@@ -132,7 +113,6 @@ export default class Notebook {
     this.canSave = vm.canSave;
 
     this.unloaded = ko.observable(false);
-    this.updateHistoryFailed = false;
 
     this.viewSchedulerId = ko.observable(notebookRaw.viewSchedulerId || '');
     this.viewSchedulerId.subscribe(() => {
@@ -158,12 +138,6 @@ export default class Notebook {
           this.presentationSnippets()[key] = _snippet;
         });
       }
-      if (vm.editorMode() && this.history().length === 0) {
-        this.fetchHistory(() => {
-          this.updateHistory(['starting', 'running'], 30000);
-          this.updateHistory(['available'], 60000 * 5);
-        });
-      }
     }
 
     huePubSub.subscribeOnce(
@@ -277,46 +251,6 @@ export default class Notebook {
     this.snippets()[this.executingAllIndex()].execute();
   }
 
-  fetchHistory(callback) {
-    const QUERIES_PER_PAGE = 50;
-    this.loadingHistory(true);
-
-    $.get(
-      '/notebook/api/get_history',
-      {
-        doc_type: this.selectedSnippet(),
-        limit: QUERIES_PER_PAGE,
-        page: this.historyCurrentPage(),
-        doc_text: this.historyFilter(),
-        is_notification_manager: this.parentVm.isNotificationManager()
-      },
-      data => {
-        const parsedHistory = [];
-        if (data && data.history) {
-          data.history.forEach(nbk => {
-            parsedHistory.push(
-              this.makeHistoryRecord(
-                nbk.absoluteUrl,
-                nbk.data.statement,
-                nbk.data.lastExecuted,
-                nbk.data.status,
-                nbk.name,
-                nbk.uuid
-              )
-            );
-          });
-        }
-        this.history(parsedHistory);
-        this.historyTotalPages(Math.ceil(data.count / QUERIES_PER_PAGE));
-      }
-    ).always(() => {
-      this.loadingHistory(false);
-      if (callback) {
-        callback();
-      }
-    });
-  }
-
   getSnippets(type) {
     return this.snippets().filter(snippet => snippet.type() === type);
   }
@@ -397,17 +331,6 @@ export default class Notebook {
     }
   }
 
-  makeHistoryRecord(url, statement, lastExecuted, status, name, uuid) {
-    return komapping.fromJS({
-      url: url,
-      query: statement.substring(0, 1000) + (statement.length > 1000 ? '...' : ''),
-      lastExecuted: lastExecuted,
-      status: status,
-      name: name,
-      uuid: uuid
-    });
-  }
-
   newSnippet(type) {
     if (type) {
       this.selectedSnippet(type);
@@ -439,18 +362,6 @@ export default class Notebook {
     this.snippets(this.snippets().move(this.snippets().length - 1, idx));
   }
 
-  nextHistoryPage() {
-    if (this.historyCurrentPage() < this.historyTotalPages()) {
-      this.historyCurrentPage(this.historyCurrentPage() + 1);
-    }
-  }
-
-  prevHistoryPage() {
-    if (this.historyCurrentPage() !== 1) {
-      this.historyCurrentPage(this.historyCurrentPage() - 1);
-    }
-  }
-
   async save(callback) {
     hueAnalytics.log('notebook', 'save');
 
@@ -616,53 +527,4 @@ export default class Notebook {
     });
     return currentQueries;
   }
-
-  updateHistory(statuses, interval) {
-    let items = this.history()
-      .filter(item => statuses.indexOf(item.status()) !== -1)
-      .slice(0, 25);
-
-    const updateHistoryCall = item => {
-      apiHelper
-        .checkStatus({
-          notebookJson: JSON.stringify({ uuid: item.uuid() }),
-          silenceErrors: true
-        })
-        .then(data => {
-          const status =
-            data.status === -3
-              ? 'expired'
-              : data.status === 0
-              ? data.query_status.status
-              : 'failed';
-          if (status && item.status() !== status) {
-            item.status(status);
-          }
-        })
-        .fail(() => {
-          items = [];
-          this.updateHistoryFailed = true;
-          console.warn('Lost connectivity to the Hue history refresh backend.');
-        })
-        .always(() => {
-          if (items.length > 0) {
-            window.setTimeout(() => {
-              updateHistoryCall(items.pop());
-            }, 1000);
-          } else if (!this.updateHistoryFailed) {
-            window.setTimeout(() => {
-              this.updateHistory(statuses, interval);
-            }, interval);
-          }
-        });
-    };
-
-    if (items.length > 0) {
-      updateHistoryCall(items.pop());
-    } else if (!this.updateHistoryFailed) {
-      window.setTimeout(() => {
-        this.updateHistory(statuses, interval);
-      }, interval);
-    }
-  }
 }

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

@@ -24,6 +24,7 @@ import 'apps/notebook2/components/ko.executableLogs';
 import 'apps/notebook2/components/ko.executableProgressBar';
 import 'apps/notebook2/components/ko.snippetEditorActions';
 import 'apps/notebook2/components/ko.snippetResults';
+import 'apps/notebook2/components/ko.queryHistory';
 
 import AceAutocompleteWrapper from 'apps/notebook/aceAutocompleteWrapper';
 import apiHelper from 'api/apiHelper';

+ 9 - 55
desktop/libs/notebook/src/notebook/templates/editor_components2.mako

@@ -547,61 +547,15 @@
         </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'}, style: { 'height' : $parent.historyInitialHeight() > 0 ? Math.max($parent.historyInitialHeight(), 40) + 'px' : '' }">
-            <!-- ko if: $parent.loadingHistory -->
-            <div class="margin-top-10 margin-left-10">
-              <i class="fa fa-spinner fa-spin muted"></i>
-            </div>
-            <!-- /ko -->
-
-            <!-- ko ifnot: $parent.loadingHistory -->
-            <!-- ko if: $parent.history().length === 0 && $parent.historyFilter() === '' -->
-            <div class="margin-top-10 margin-left-10" style="font-style: italic">${ _("No queries to be shown.") }</div>
-            <!-- /ko -->
-            <!-- ko if: $parent.history().length === 0 && $parent.historyFilter() !== '' -->
-            <div class="margin-top-10 margin-left-10" style="font-style: italic">${ _('No queries found for') } <strong data-bind="text: $parent.historyFilter"></strong>.</div>
-            <!-- /ko -->
-
-
-            <!-- ko if: $parent.history().length > 0 -->
-            <table class="table table-condensed margin-top-10 history-table">
-              <tbody data-bind="foreach: { data: $parent.history, afterRender: function(){ huePubSub.publish('editor.calculate.history.height'); } }">
-              <tr data-bind="click: function() { if (uuid() != $root.selectedNotebook().uuid()) { $root.openNotebook(uuid()); } }, css: { 'highlight': uuid() == $root.selectedNotebook().uuid(), 'pointer': uuid() != $root.selectedNotebook().uuid() }">
-                <td style="width: 100px" class="muted" data-bind="style: {'border-top-width': $index() == 0 ? '0' : ''}">
-                  <span data-bind="momentFromNow: {data: lastExecuted, interval: 10000, titleFormat: 'LLL'}"></span>
-                </td>
-                <td style="width: 25px" class="muted" data-bind="style: {'border-top-width': $index() == 0 ? '0' : ''}">
-                  <!-- ko switch: status -->
-                  <!-- ko case: 'running' -->
-                  <div class="history-status" data-bind="tooltip: { title: '${ _ko("Query running") }', placement: 'bottom' }"><i class="fa fa-fighter-jet fa-fw"></i></div>
-                  <!-- /ko -->
-                  <!-- ko case: 'failed' -->
-                  <div class="history-status" data-bind="tooltip: { title: '${ _ko("Query failed") }', placement: 'bottom' }"><i class="fa fa-exclamation fa-fw"></i></div>
-                  <!-- /ko -->
-                  <!-- ko case: 'available' -->
-                  <div class="history-status" data-bind="tooltip: { title: '${ _ko("Result available") }', placement: 'bottom' }"><i class="fa fa-check fa-fw"></i></div>
-                  <!-- /ko -->
-                  <!-- ko case: 'expired' -->
-                  <div class="history-status" data-bind="tooltip: { title: '${ _ko("Result expired") }', placement: 'bottom' }"><i class="fa fa-unlink fa-fw"></i></div>
-                  <!-- /ko -->
-                  <!-- /ko -->
-                </td>
-                <td style="width: 25px" class="muted" data-bind="ellipsis: {data: name(), length: 30}, style: {'border-top-width': $index() == 0 ? '0' : ''}"></td>
-                <td data-bind="style: {'border-top-width': $index() == 0 ? '0' : ''}, click: function(){ if (window.getSelection().toString() === '' && uuid() != $root.selectedNotebook().uuid()) { $root.openNotebook(uuid()) }  }, clickBubble: false"><div data-bind="highlight: { value: query, dialect: $parent.type }"></div></td>
-              </tr>
-              </tbody>
-            </table>
-            <!-- /ko -->
-            <!-- ko with: $parent -->
-            <div class="pagination" data-bind="visible: historyTotalPages() > 1">
-              <ul>
-                <li data-bind="css: { 'disabled' : historyCurrentPage() === 1 }"><a href="javascript: void(0);" data-bind="click: function() { prevHistoryPage(); }">${ _("Prev") }</a></li>
-                <li class="active"><span data-bind="text: historyCurrentPage() + '/' + historyTotalPages()"></span></li>
-                <li data-bind="css: { 'disabled' : historyCurrentPage() === historyTotalPages() }"><a href="javascript: void(0);" data-bind="click: function() { nextHistoryPage(); }">${ _("Next") }</a></li>
-              </ul>
-            </div>
-            <!-- /ko -->
-            <!-- /ko -->
+          <div class="tab-pane" id="queryHistory" style="min-height: 80px;" data-bind="css: {'active': currentQueryTab() == 'queryHistory'}">
+            <!-- ko component: {
+              name: 'query-history',
+              params: {
+                currentNotebook: parentNotebook,
+                openFunction: parentVm.openNotebook.bind(parentVm),
+                type: type
+              }
+            } --><!-- /ko -->
           </div>
 
           <div class="tab-pane" id="savedQueries" data-bind="css: {'active': currentQueryTab() == 'savedQueries'}" style="overflow: hidden">