Răsfoiți Sursa

[frontend] First version of predict typeahead in editor v2

Johan Ahlen 4 ani în urmă
părinte
comite
e519d15dca

+ 10 - 7
desktop/core/src/desktop/js/apps/editor/components/aceEditor/AceEditor.vue

@@ -37,6 +37,7 @@
   import ace, { getAceMode } from 'ext/aceHelper';
   import { Ace } from 'ext/ace';
 
+  import { attachPredictTypeahead } from './acePredict';
   import AceAutocomplete from './autocomplete/AceAutocomplete.vue';
   import AceGutterHandler from './AceGutterHandler';
   import AceLocationHandler from './AceLocationHandler';
@@ -103,7 +104,7 @@
 
       if (this.sqlParserProvider) {
         this.sqlParserProvider
-          .getAutocompleteParser(this.executor.connector().dialect)
+          .getAutocompleteParser(this.executor.connector().dialect || 'generic')
           .then(autocompleteParser => {
             this.autocompleteParser = autocompleteParser;
           });
@@ -209,15 +210,17 @@
         e.text = removeUnicodes(e.text);
       };
 
+      if ((<hueWindow>window).ENABLE_PREDICT) {
+        attachPredictTypeahead(editor, this.executor.connector());
+      }
+
       let placeholderVisible = false;
       const placeholderElement = this.createPlaceholderElement();
       const onInput = () => {
-        if (!placeholderVisible) {
-          if (!editor.getValue().length) {
-            editor.renderer.scroller.append(placeholderElement);
-            placeholderVisible = true;
-          }
-        } else {
+        if (!placeholderVisible && !editor.getValue().length) {
+          editor.renderer.scroller.append(placeholderElement);
+          placeholderVisible = true;
+        } else if (placeholderVisible) {
           placeholderElement.remove();
           placeholderVisible = false;
         }

+ 115 - 0
desktop/core/src/desktop/js/apps/editor/components/aceEditor/acePredict.ts

@@ -0,0 +1,115 @@
+// 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 { Ace } from 'ext/ace';
+import { Disposable } from 'components/utils/SubscriptionTracker';
+import { Connector } from 'types/config';
+import { getOptimizer, PredictResponse } from 'catalog/optimizer/optimizer';
+import { CancellablePromise } from 'api/cancellablePromise';
+import { defer } from 'utils/hueUtils';
+
+type ActivePredict = { text: string; element: HTMLElement };
+
+export const attachPredictTypeahead = (editor: Ace.Editor, connector: Connector): Disposable => {
+  let activePredict: { text: string; element: HTMLElement } | undefined;
+
+  const optimizer = getOptimizer(connector);
+
+  const addPredictElement = (text: string): ActivePredict => {
+    const element = document.createElement('div');
+    element.innerText = text;
+    element.style.marginLeft = '4px';
+    element.classList.add('ace_invisible');
+    element.classList.add('ace_emptyMessage');
+    editor.renderer.scroller.append(element);
+    return { text, element };
+  };
+
+  const removeActivePredict = (): void => {
+    if (activePredict) {
+      activePredict.element.remove();
+      activePredict = undefined;
+    }
+  };
+
+  const setActivePredict = (text: string): void => {
+    removeActivePredict();
+    activePredict = addPredictElement(text);
+  };
+
+  let activePredictPromise: CancellablePromise<PredictResponse> | undefined;
+
+  const updatePredictTypeahead = () => {
+    if (activePredictPromise) {
+      activePredictPromise.cancel();
+    }
+    defer(() => {
+      const editorText = editor.getValue();
+      if (
+        activePredict &&
+        (!editorText.length ||
+          activePredict.text === editorText ||
+          activePredict.text.indexOf(editorText) !== 0)
+      ) {
+        removeActivePredict();
+      }
+      if (editorText.length && !activePredict) {
+        optimizer
+          .predict({
+            beforeCursor: editor.getTextBeforeCursor(),
+            afterCursor: editor.getTextAfterCursor()
+          })
+          .then(response => {
+            if (response.prediction) {
+              setActivePredict(response.prediction);
+            } else {
+              removeActivePredict();
+            }
+          })
+          .catch(() => {
+            removeActivePredict();
+          });
+      }
+    });
+  };
+
+  editor.commands.addCommand({
+    name: 'applyPredict',
+    bindKey: { win: 'Tab', mac: 'Tab' },
+    exec: () => {
+      if (activePredict) {
+        editor.setValue(activePredict.text, 1);
+        editor.clearSelection();
+      } else {
+        editor.indent();
+      }
+    },
+    multiSelectAction: 'forEach',
+    scrollIntoView: 'selectionPart'
+  });
+
+  const predictOnInput = () => {
+    updatePredictTypeahead();
+  };
+
+  editor.on('input', predictOnInput);
+
+  return {
+    dispose: () => {
+      editor.off('input', predictOnInput);
+    }
+  };
+};

+ 20 - 0
desktop/core/src/desktop/js/catalog/optimizer/ApiStrategy.ts

@@ -25,6 +25,8 @@ import {
   Optimizer,
   OptimizerRisk,
   PopularityOptions,
+  PredictOptions,
+  PredictResponse,
   RiskOptions,
   SimilarityOptions
 } from 'catalog/optimizer/optimizer';
@@ -60,6 +62,7 @@ const genericOptimizerMultiTableFetch = <T extends TimestampedData>(
 };
 
 const COMPATIBILITY_URL = '/notebook/api/optimizer/statement/compatibility';
+const PREDICT_URL = '/metadata/api/optimizer/predict';
 const RISK_URL = '/notebook/api/optimizer/statement/risk';
 const SIMILARITY_URL = '/notebook/api/optimizer/statement/similarity';
 const TOP_AGGS_URL = '/metadata/api/optimizer/top_aggs';
@@ -211,4 +214,21 @@ export default class ApiStrategy implements Optimizer {
       }
     );
   }
+
+  predict({ beforeCursor, afterCursor }: PredictOptions): CancellablePromise<PredictResponse> {
+    return post<PredictResponse>(
+      PREDICT_URL,
+      {
+        connector: JSON.stringify(this.connector),
+        beforeCursor,
+        afterCursor
+      },
+      {
+        silenceErrors: true,
+        handleSuccess: (response: PredictResponse, resolve) => {
+          resolve(response);
+        }
+      }
+    );
+  }
 }

+ 7 - 0
desktop/core/src/desktop/js/catalog/optimizer/NoopSqlAnalyzer.ts

@@ -23,6 +23,8 @@ import {
   Optimizer,
   OptimizerRisk,
   PopularityOptions,
+  PredictOptions,
+  PredictResponse,
   RiskOptions,
   SimilarityOptions
 } from 'catalog/optimizer/optimizer';
@@ -74,4 +76,9 @@ export default class NoopSqlAnalyzer implements Optimizer {
   fetchTopJoins(options: PopularityOptions): CancellablePromise<TopJoins> {
     return CancellablePromise.reject('fetchTopJoins is not Implemented');
   }
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  predict(options: PredictOptions): CancellablePromise<PredictResponse> {
+    return CancellablePromise.reject('predict is not Implemented');
+  }
 }

+ 6 - 0
desktop/core/src/desktop/js/catalog/optimizer/SqlAnalyzer.ts

@@ -26,6 +26,8 @@ import {
   Optimizer,
   OptimizerRisk,
   PopularityOptions,
+  PredictOptions,
+  PredictResponse,
   RiskHint,
   RiskOptions,
   SimilarityOptions
@@ -246,4 +248,8 @@ export default class SqlAnalyzer implements Optimizer {
     }
     return CancellablePromise.reject('fetchTopFilters is not Implemented');
   }
+
+  predict(options: PredictOptions): CancellablePromise<PredictResponse> {
+    return this.apiStrategy.predict(options);
+  }
 }

+ 10 - 0
desktop/core/src/desktop/js/catalog/optimizer/optimizer.ts

@@ -72,6 +72,15 @@ export interface MetaOptions {
   silenceErrors?: boolean;
 }
 
+export interface PredictOptions {
+  beforeCursor: string;
+  afterCursor: string;
+}
+
+export interface PredictResponse {
+  prediction?: string;
+}
+
 export interface Optimizer {
   analyzeRisk(options: RiskOptions): CancellablePromise<OptimizerRisk>;
   analyzeSimilarity(options: SimilarityOptions): CancellablePromise<unknown>;
@@ -82,6 +91,7 @@ export interface Optimizer {
   fetchTopFilters(options: PopularityOptions): CancellablePromise<TopFilters>;
   fetchTopJoins(options: PopularityOptions): CancellablePromise<TopJoins>;
   fetchOptimizerMeta(options: MetaOptions): CancellablePromise<OptimizerMeta>;
+  predict(options: PredictOptions): CancellablePromise<PredictResponse>;
 }
 
 const optimizerInstances: { [connectorId: string]: Optimizer | undefined } = {};

+ 4 - 1
desktop/core/src/desktop/js/ext/ace.d.ts

@@ -27,7 +27,9 @@ declare namespace Ace {
       addCommand(command: {
         name: string,
         bindKey: { win: string; mac: string } | string,
-        exec(): void
+        exec(): void,
+        multiSelectAction?: string,
+        scrollIntoView?: string
       }): void;
       bindKey(key: { win: string; mac: string } | string, command: string): void;
     }
@@ -46,6 +48,7 @@ declare namespace Ace {
     getTextAfterCursor(): string;
     getTextBeforeCursor(): string;
     getValue(): string;
+    indent(): void;
     keyBinding: {
       addKeyboardHandler(hashHandler: HashHandler): void;
       removeKeyboardHandler(hashHandler: HashHandler): void;