Pārlūkot izejas kodu

HUE-9000 [editor] Add query explain in notebook 2

Johan Ahlen 6 gadi atpakaļ
vecāks
revīzija
5ab6ff78d2

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

@@ -2130,6 +2130,27 @@ class ApiHelper {
     return promise;
   }
 
+  /**
+   *
+   * @param {Object} options
+   * @param {Snippet} options.snippet
+   *
+   * @return {CancellablePromise<string>}
+   */
+  async explainAsync(options) {
+    const data = {
+      notebook: await options.snippet.parentNotebook.toContextJson(),
+      snippet: options.snippet.toContextJson()
+    };
+    return new Promise((resolve, reject) => {
+      this.simplePost('/notebook/api/explain', data, options)
+        .done(response => {
+          resolve(response.explanation);
+        })
+        .fail(reject);
+    });
+  }
+
   /**
    *
    * @param {Object} options

+ 5 - 19
desktop/core/src/desktop/js/apps/notebook2/components/ko.snippetEditorActions.js

@@ -14,7 +14,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import $ from 'jquery';
 import ko from 'knockout';
 
 import 'ko/bindings/ko.publish';
@@ -23,7 +22,6 @@ import apiHelper from 'api/apiHelper';
 import componentUtils from 'ko/components/componentUtils';
 import hueAnalytics from 'utils/hueAnalytics';
 import I18n from 'utils/i18n';
-import { notebookToContextJSON, snippetToContextJSON } from 'apps/notebook2/notebookSerde';
 import { STATUS } from 'apps/notebook2/snippet';
 
 const TEMPLATE = `
@@ -133,28 +131,16 @@ class SnippetEditorActions {
     this.snippet.status(STATUS.ready);
   }
 
-  explain() {
+  async explain() {
     if (!this.explainEnabled()) {
       return;
     }
     hueAnalytics.log('notebook', 'explain');
 
-    this.snippet.result.explanation('');
-    this.snippet.errors([]);
-    this.snippet.status(STATUS.ready);
-
-    $.post('/notebook/api/explain', {
-      notebook: notebookToContextJSON(this.snippet.parentNotebook),
-      snippet: snippetToContextJSON(this.snippet)
-    }).then(data => {
-      if (data.status === 0) {
-        this.snippet.currentQueryTab('queryExplain');
-        this.snippet.result.fetchedOnce(true);
-        this.snippet.result.explanation(data.explanation);
-      } else {
-        this.snippet.handleAjaxError(data);
-      }
-    });
+    this.snippet.explanation('');
+    const explanation = await apiHelper.explainAsync({ snippet: this.snippet });
+    this.snippet.explanation(explanation);
+    this.snippet.currentQueryTab('queryExplain');
   }
 
   format() {

+ 23 - 11
desktop/core/src/desktop/js/apps/notebook2/notebook.js

@@ -22,9 +22,9 @@ import apiHelper from 'api/apiHelper';
 import hueAnalytics from 'utils/hueAnalytics';
 import huePubSub from 'utils/huePubSub';
 import hueUtils from 'utils/hueUtils';
+import sessionManager from 'apps/notebook2/execution/sessionManager';
 
 import Snippet, { STATUS as SNIPPET_STATUS } from 'apps/notebook2/snippet';
-import { notebookToContextJSON } from 'apps/notebook2/notebookSerde';
 
 export default class Notebook {
   constructor(vm, notebook) {
@@ -225,11 +225,11 @@ export default class Notebook {
     return newSnippet;
   }
 
-  clearHistory() {
+  async clearHistory() {
     hueAnalytics.log('notebook', 'clearHistory');
     apiHelper
       .clearNotebookHistory({
-        notebookJson: notebookToContextJSON(this),
+        notebookJson: await this.toContextJson(),
         docType: this.selectedSnippet(),
         isNotificationManager: this.parentVm.isNotificationManager()
       })
@@ -258,10 +258,10 @@ export default class Notebook {
     });
   }
 
-  close() {
+  async close() {
     hueAnalytics.log('notebook', 'close');
     apiHelper.closeNotebook({
-      notebookJson: this.toJson(),
+      notebookJson: await this.toJson(),
       editorMode: this.parentVm.editorMode()
     });
   }
@@ -460,7 +460,7 @@ export default class Notebook {
 
     try {
       const data = await apiHelper.saveNotebook({
-        notebookJson: this.toJson(),
+        notebookJson: await this.toJson(),
         editorMode: editorMode
       });
 
@@ -559,7 +559,19 @@ export default class Notebook {
     });
   }
 
-  toJs() {
+  async toContextJson() {
+    return JSON.stringify({
+      id: this.id(),
+      isSaved: this.isSaved(),
+      name: this.name(),
+      parentSavedQueryUuid: this.parentSavedQueryUuid(),
+      sessions: await sessionManager.getAllSessions(),
+      type: this.type(),
+      uuid: this.uuid()
+    });
+  }
+
+  async toJs() {
     return {
       coordinatorUuid: this.coordinatorUuid(),
       description: this.description(),
@@ -578,7 +590,7 @@ export default class Notebook {
       presentationSnippets: this.presentationSnippets(),
       pubSubUrl: this.pubSubUrl(),
       result: {}, // TODO: Moved to executor but backend requires it
-      sessions: [], // TODO: Moved to executor but backend requires it
+      sessions: await sessionManager.getAllSessions(),
       snippets: this.snippets().map(snippet => snippet.toJs()),
       type: this.type(),
       uuid: this.uuid(),
@@ -586,8 +598,8 @@ export default class Notebook {
     };
   }
 
-  toJson() {
-    return JSON.stringify(this.toJs());
+  async toJson() {
+    return JSON.stringify(await this.toJs());
   }
 
   unload() {
@@ -612,7 +624,7 @@ export default class Notebook {
 
     const updateHistoryCall = item => {
       apiHelper
-        .checkStatus({ notebookJson: komapping.toJSON({ id: item.uuid() }) })
+        .checkStatus({ notebookJson: JSON.stringify({ id: item.uuid() }) })
         .then(data => {
           const status =
             data.status === -3

+ 0 - 44
desktop/core/src/desktop/js/apps/notebook2/notebookSerde.js

@@ -1,44 +0,0 @@
-// 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 komapping from 'knockout.mapping';
-
-export const snippetToContextJSON = snippet =>
-  JSON.stringify({
-    id: snippet.id(),
-    type: snippet.type(),
-    status: snippet.status(),
-    statementType: snippet.statementType(),
-    statement: snippet.statement(),
-    aceCursorPosition: snippet.aceCursorPosition(),
-    statementPath: snippet.statementPath(),
-    associatedDocumentUuid: snippet.associatedDocumentUuid(),
-    properties: komapping.toJS(snippet.properties), // TODO: Drop komapping
-    result: {}, // TODO: Moved to executor but backend requires it
-    database: snippet.database(),
-    compute: snippet.compute(),
-    wasBatchExecuted: snippet.wasBatchExecuted()
-  });
-
-export const notebookToContextJSON = notebook =>
-  JSON.stringify({
-    id: notebook.id(),
-    isSaved: notebook.isSaved(),
-    name: notebook.name(),
-    parentSavedQueryUuid: notebook.parentSavedQueryUuid(),
-    type: notebook.type(),
-    uuid: notebook.uuid()
-  });

+ 35 - 16
desktop/core/src/desktop/js/apps/notebook2/snippet.js

@@ -33,7 +33,6 @@ import huePubSub from 'utils/huePubSub';
 import hueUtils from 'utils/hueUtils';
 import sessionManager from 'apps/notebook2/execution/sessionManager';
 import SqlExecutable from 'apps/notebook2/execution/sqlExecutable';
-import { notebookToContextJSON, snippetToContextJSON } from 'apps/notebook2/notebookSerde';
 import { REDRAW_FIXED_HEADERS_EVENT } from 'apps/notebook2/events';
 import { EXECUTABLE_UPDATED_EVENT, EXECUTION_STATUS } from 'apps/notebook2/execution/executable';
 import {
@@ -236,6 +235,8 @@ export default class Snippet {
 
     this.editorMode = this.parentVm.editorMode;
 
+    this.explanation = ko.observable();
+
     this.getAceMode = () => this.parentVm.getSnippetViewSettings(this.type()).aceMode;
 
     this.dbSelectionVisible = ko.observable(false);
@@ -888,7 +889,7 @@ export default class Snippet {
         }
       });
 
-      this.checkComplexity = () => {
+      this.checkComplexity = async () => {
         if (!this.inFocus() || lastCheckedComplexityStatement === this.statement()) {
           return;
         }
@@ -921,8 +922,8 @@ export default class Snippet {
         if (unknownResponse) {
           lastComplexityRequest = apiHelper
             .statementRisk({
-              notebookJson: notebookToContextJSON(this.parentNotebook),
-              snippetJson: snippetToContextJSON(this)
+              notebookJson: await this.parentNotebook.toContextJson(),
+              snippetJson: this.toContextJson()
             })
             .then(data => {
               knownResponses.unshift({
@@ -1075,10 +1076,10 @@ export default class Snippet {
     this.queryCompatibility();
   }
 
-  close() {
+  async close() {
     $.post('/notebook/api/close_statement', {
-      notebook: notebookToContextJSON(this.parentNotebook),
-      snippet: snippetToContextJSON(this)
+      notebook: await this.parentNotebook.toContextJson(),
+      snippet: this.toContextJson()
     });
   }
 
@@ -1187,12 +1188,12 @@ export default class Snippet {
     });
   }
 
-  getExternalStatement() {
+  async getExternalStatement() {
     this.externalStatementLoaded(false);
     apiHelper
       .getExternalStatement({
-        notebookJson: notebookToContextJSON(this.parentNotebook),
-        snippetJson: snippetToContextJSON(this)
+        notebookJson: await this.parentNotebook.toContextJson(),
+        snippetJson: this.toContextJson()
       })
       .then(data => {
         if (data.status === 0) {
@@ -1270,13 +1271,13 @@ export default class Snippet {
     return this.parentVm.getSnippetViewSettings(this.type()).placeHolder;
   }
 
-  getSimilarQueries() {
+  async getSimilarQueries() {
     hueAnalytics.log('notebook', 'get_query_similarity');
 
     apiHelper
       .statementSimilarity({
-        notebookJson: notebookToContextJSON(this.parentNotebook),
-        snippetJson: snippetToContextJSON(this),
+        notebookJson: await this.parentNotebook.toContextJson(),
+        snippetJson: this.toContextJson(),
         sourcePlatform: this.type()
       })
       .then(data => {
@@ -1412,7 +1413,7 @@ export default class Snippet {
     }
   }
 
-  queryCompatibility(targetPlatform) {
+  async queryCompatibility(targetPlatform) {
     apiHelper.cancelActiveRequest(this.lastCompatibilityRequest);
 
     hueAnalytics.log('notebook', 'compatibility');
@@ -1422,8 +1423,8 @@ export default class Snippet {
 
     this.lastCompatibilityRequest = apiHelper
       .statementCompatibility({
-        notebookJson: notebookToContextJSON(this.parentNotebook),
-        snippetJson: snippetToContextJSON(this),
+        notebookJson: await this.parentNotebook.toContextJson(),
+        snippetJson: this.toContextJson(),
         sourcePlatform: this.compatibilitySourcePlatform().value,
         targetPlatform: this.compatibilityTargetPlatform().value
       })
@@ -1500,6 +1501,24 @@ export default class Snippet {
     this.showLongOperationWarning(false);
   }
 
+  toContextJson() {
+    return JSON.stringify({
+      id: this.id(),
+      type: this.type(),
+      status: this.status(),
+      statementType: this.statementType(),
+      statement: this.statement(),
+      aceCursorPosition: this.aceCursorPosition(),
+      statementPath: this.statementPath(),
+      associatedDocumentUuid: this.associatedDocumentUuid(),
+      properties: komapping.toJS(this.properties), // TODO: Drop komapping
+      result: {}, // TODO: Moved to executor but backend requires it
+      database: this.database(),
+      compute: this.compute(),
+      wasBatchExecuted: this.wasBatchExecuted()
+    });
+  }
+
   toJs() {
     return {
       aceCursorPosition: this.aceCursorPosition(),

+ 3 - 16
desktop/core/src/desktop/js/apps/notebook2/spec/notebookSerdeSpec.js → desktop/core/src/desktop/js/apps/notebook2/spec/notebookSpec.js

@@ -14,11 +14,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { notebookToContextJSON, snippetToContextJSON } from '../notebookSerde';
 import Notebook from '../notebook';
 import sessionManager from 'apps/notebook2/execution/sessionManager';
 
-describe('notebookSerde.js', () => {
+describe('notebook.js', () => {
   const viewModel = {
     editorType: () => 'notebook',
     selectedNotebook: () => undefined,
@@ -42,7 +41,7 @@ describe('notebookSerde.js', () => {
     notebook.addSnippet({ type: 'hive' });
     notebook.addSnippet({ type: 'impala' });
 
-    const notebookJSON = notebook.toJson();
+    const notebookJSON = await notebook.toJson();
 
     const notebookRaw = JSON.parse(notebookJSON);
 
@@ -55,22 +54,10 @@ describe('notebookSerde.js', () => {
     const notebook = new Notebook(viewModel, {});
     notebook.addSnippet({ type: 'hive' });
 
-    const notebookContextJSON = notebookToContextJSON(notebook);
+    const notebookContextJSON = await notebook.toContextJson();
 
     const notebookContextRaw = JSON.parse(notebookContextJSON);
 
     expect(notebookContextRaw.id).toEqual(notebook.id());
   });
-
-  it('should serialize a snippet context to JSON', async () => {
-    const notebook = new Notebook(viewModel, {});
-    const snippet = notebook.addSnippet({ type: 'hive' });
-
-    const snippetContextJSON = snippetToContextJSON(snippet);
-
-    const snippetContextRaw = JSON.parse(snippetContextJSON);
-
-    expect(snippetContextRaw.id).toEqual(snippet.id());
-    expect(snippetContextRaw.type).toEqual('hive');
-  });
 });

+ 50 - 0
desktop/core/src/desktop/js/apps/notebook2/spec/snippetSpec.js

@@ -0,0 +1,50 @@
+// 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 Notebook from '../notebook';
+import sessionManager from 'apps/notebook2/execution/sessionManager';
+
+describe('snippet.js', () => {
+  const viewModel = {
+    editorType: () => 'notebook',
+    selectedNotebook: () => undefined,
+    availableSnippets: () => ({}),
+    editorMode: () => false,
+    getSnippetViewSettings: () => ({ sqlDialect: true })
+  };
+
+  window.HUE_CHARTS = {
+    TYPES: {
+      BARCHART: 'barchart'
+    }
+  };
+
+  beforeEach(() => {
+    spyOn(sessionManager, 'getSession').and.callFake(() => Promise.resolve());
+  });
+
+  it('should serialize a snippet context to JSON', async () => {
+    const notebook = new Notebook(viewModel, {});
+    const snippet = notebook.addSnippet({ type: 'hive' });
+
+    const snippetContextJSON = snippet.toContextJson();
+
+    const snippetContextRaw = JSON.parse(snippetContextJSON);
+
+    expect(snippetContextRaw.id).toEqual(snippet.id());
+    expect(snippetContextRaw.type).toEqual('hive');
+  });
+});

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

@@ -520,16 +520,16 @@
             <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
-          <li data-bind="click: function(){ currentQueryTab('queryResults'); }, css: {'active': currentQueryTab() == 'queryResults'}">
+          <li data-bind="click: function() { currentQueryTab('queryResults'); }, css: {'active': currentQueryTab() == 'queryResults'}">
             <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>
           </li>
-##           <!-- ko if: result.explanation().length > 0 -->
-##           <li data-bind="click: function(){ currentQueryTab('queryExplain'); }, css: {'active': currentQueryTab() == 'queryExplain'}"><a class="inactive-action" href="#queryExplain" data-toggle="tab">${_('Explain')}</a></li>
-##           <!-- /ko -->
+          <!-- ko if: explanation -->
+          <li data-bind="click: function() { currentQueryTab('queryExplain'); }, css: {'active': currentQueryTab() == 'queryExplain'}"><a class="inactive-action" href="#queryExplain" data-toggle="tab">${_('Explain')}</a></li>
+          <!-- /ko -->
           <!-- ko foreach: pinnedContextTabs -->
           <li data-bind="click: function() { $parent.currentQueryTab(tabId) }, css: { 'active': $parent.currentQueryTab() === tabId }">
             <a class="inactive-action" data-toggle="tab" data-bind="attr: { 'href': '#' + tabId }">
@@ -681,11 +681,11 @@
             <!-- /ko -->
           </div>
 
-##           <!-- ko if: result.explanation().length > 0 -->
-##           <div class="tab-pane" id="queryExplain" data-bind="css: {'active': currentQueryTab() == 'queryExplain'}">
-##             <!-- ko template: { name: 'snippet-explain${ suffix }' } --><!-- /ko -->
-##           </div>
-##           <!-- /ko -->
+          <!-- ko if: explanation -->
+          <div class="tab-pane" id="queryExplain" data-bind="css: {'active': currentQueryTab() == 'queryExplain'}">
+            <pre class="no-margin-bottom" data-bind="text: explanation"></pre>
+          </div>
+          <!-- /ko -->
 
           <!-- ko if: HAS_WORKLOAD_ANALYTICS && type() === 'impala' -->
           <div class="tab-pane" id="executionAnalysis" data-bind="css: {'active': currentQueryTab() == 'executionAnalysis'}" style="padding: 10px;">
@@ -1131,10 +1131,6 @@
     </div>
   </script>
 
-##   <script type="text/html" id="snippet-explain${ suffix }">
-##     <pre class="no-margin-bottom" data-bind="text: result.explanation"></pre>
-##   </script>
-
   <script type="text/html" id="text-snippet-body${ suffix }">
     <div data-bind="attr: {'id': 'editor_' + id()}, html: statement_raw, value: statement_raw, medium: {}" data-placeHolder="${ _('Type your text here, select some text to format it') }" class="text-snippet"></div>
   </script>