Przeglądaj źródła

HUE-7738 [editor] Update the functions panel to use the new sql functions repository

Johan Ahlen 5 lat temu
rodzic
commit
0ff59cd0f4

+ 147 - 136
desktop/core/src/desktop/js/ko/components/assist/ko.assistFunctionsPanel.js

@@ -19,9 +19,10 @@ import * as ko from 'knockout';
 import apiHelper from 'api/apiHelper';
 import componentUtils from 'ko/components/componentUtils';
 import huePubSub from 'utils/huePubSub';
-import { PigFunctions, SqlFunctions } from 'sql/sqlFunctions';
 import I18n from 'utils/i18n';
 import { CONFIG_REFRESHED_EVENT, filterEditorConnectors } from 'utils/hueConfig';
+import { getUdfCategories, hasUdfCategories } from 'sql/reference/sqlReferenceRepository';
+import { HUE_DROP_DOWN_COMPONENT } from 'ko/components/ko.dropDown';
 
 export const NAME = 'assist-functions-panel';
 // prettier-ignore
@@ -30,89 +31,98 @@ const TEMPLATE = `
     <div class="assist-flex-panel">
       <div class="assist-flex-header">
         <div class="assist-inner-header">
-          <div class="function-dialect-dropdown" data-bind="component: { name: 'hue-drop-down', params: { fixedPosition: true, value: activeDialect, entries: availableDialects, linkTitle: '${I18n(
-            'Selected dialect'
-          )}' } }" style="display: inline-block"></div>
+          <div class="function-dialect-dropdown" data-bind="
+              component: {
+                name: '${ HUE_DROP_DOWN_COMPONENT }',
+                params: {
+                  fixedPosition: true,
+                  value: activeConnectorUdfs,
+                  entries: availableConnectorUdfs,
+                  linkTitle: '${I18n('Selected type')}'
+                 }
+               }
+             " style="display: inline-block"></div>
         </div>
       </div>
-      <div class="assist-flex-search">
-        <div class="assist-filter">
-          <form autocomplete="off">
-            <input class="clearable" type="text" ${ window.PREVENT_AUTOFILL_INPUT_ATTRS } placeholder="Filter..." data-bind="clearable: query, value: query, valueUpdate: 'afterkeydown'">
-          </form>
-        </div>
-      </div>
-      <div data-bind="css: { 'assist-flex-fill': !selectedFunction(), 'assist-flex-half': selectedFunction() }">
-        <!-- ko ifnot: query -->
-        <ul class="assist-function-categories" data-bind="foreach: activeCategories">
-          <li>
-            <a class="black-link" href="javascript: void(0);" data-bind="toggle: open"><i class="fa fa-fw" data-bind="css: { 'fa-chevron-right': !open(), 'fa-chevron-down': open }"></i> <span data-bind="text: name"></span></a>
-            <ul class="assist-functions" data-bind="slideVisible: open, foreach: functions">
-              <li data-bind="tooltip: { title: description, placement: 'left', delay: 1000 }">
-                <a class="assist-field-link" href="javascript: void(0);" data-bind="draggableText: { text: draggable, meta: { type: 'function' } }, css: { 'blue': $parents[1].selectedFunction() === $data }, multiClick: { click: function () { $parents[1].selectedFunction($data); }, dblClick: function () { huePubSub.publish('editor.insert.at.cursor', { text: draggable }); } }, text: signature"></a>
-              </li>
-            </ul>
-          </li>
-        </ul>
-        <!-- /ko -->
-        <!-- ko if: query -->
-        <!-- ko if: filteredFunctions().length > 0 -->
-        <ul class="assist-functions" data-bind="foreach: filteredFunctions">
-          <li data-bind="tooltip: { title: description, placement: 'left', delay: 1000 }">
-            <a class="assist-field-link" href="javascript: void(0);" data-bind="draggableText: { text: draggable, meta: { type: 'function' } }, css: { 'blue': $parent.selectedFunction() === $data }, multiClick: { click: function () { $parent.selectedFunction($data); }, dblClick: function () { huePubSub.publish('editor.insert.at.cursor', { text: draggable }); } }, html: signatureMatch"></a>
-          </li>
-        </ul>
-        <!-- /ko -->
-        <!-- ko if: filteredFunctions().length === 0 -->
-        <ul class="assist-functions">
-          <li class="assist-no-entries">${I18n('No results found.')}</li>
-        </ul>
-        <!-- /ko -->
+      <!-- ko with: activeConnectorUdfs -->
+        <!-- ko hueSpinner: { spin: loading, center: true, size: 'large' } --><!-- /ko -->
+        <!-- ko ifnot: loading -->
+          <div class="assist-flex-search">
+            <div class="assist-filter">
+              <form autocomplete="off">
+                <input class="clearable" type="text" ${ window.PREVENT_AUTOFILL_INPUT_ATTRS } placeholder="Filter..." data-bind="clearable: query, value: query, valueUpdate: 'afterkeydown'">
+              </form>
+            </div>
+          </div>
+          <div data-bind="css: { 'assist-flex-fill': !selectedFunction(), 'assist-flex-half': selectedFunction() }">
+            <!-- ko ifnot: query -->
+              <ul class="assist-function-categories" data-bind="foreach: categories">
+                <li>
+                  <a class="black-link" href="javascript: void(0);" data-bind="toggle: open"><i class="fa fa-fw" data-bind="css: { 'fa-chevron-right': !open(), 'fa-chevron-down': open }"></i> <span data-bind="text: name"></span></a>
+                  <ul class="assist-functions" data-bind="slideVisible: open, foreach: functions">
+                    <li data-bind="tooltip: { title: description, placement: 'left', delay: 1000 }">
+                      <a class="assist-field-link" href="javascript: void(0);" data-bind="draggableText: { text: draggable, meta: { type: 'function' } }, css: { 'blue': $parents[1].selectedFunction() === $data }, multiClick: { click: function () { $parents[1].selectedFunction($data); }, dblClick: function () { huePubSub.publish('editor.insert.at.cursor', { text: draggable }); } }, text: signature"></a>
+                    </li>
+                  </ul>
+                </li>
+              </ul>
+            <!-- /ko -->
+            <!-- ko if: query -->
+              <!-- ko if: filteredFunctions().length > 0 -->
+              <ul class="assist-functions" data-bind="foreach: filteredFunctions">
+                <li data-bind="tooltip: { title: description, placement: 'left', delay: 1000 }">
+                  <a class="assist-field-link" href="javascript: void(0);" data-bind="draggableText: { text: draggable, meta: { type: 'function' } }, css: { 'blue': $parent.selectedFunction() === $data }, multiClick: { click: function () { $parent.selectedFunction($data); }, dblClick: function () { huePubSub.publish('editor.insert.at.cursor', { text: draggable }); } }, html: signatureMatch"></a>
+                </li>
+              </ul>
+              <!-- /ko -->
+              <!-- ko if: filteredFunctions().length === 0 -->
+              <ul class="assist-functions">
+                <li class="assist-no-entries">${I18n('No results found.')}</li>
+              </ul>
+              <!-- /ko -->
+            <!-- /ko -->
+          </div>
+          <!-- ko if: selectedFunction -->
+            <div class="assist-flex-half assist-function-details" data-bind="with: selectedFunction">
+              <div class="assist-panel-close"><button class="close" data-bind="click: function() { $parent.selectedFunction(null); }">&times;</button></div>
+              <div class="assist-function-signature blue" data-bind="draggableText: { text: draggable, meta: { type: 'function' } }, text: signature, event: { 'dblclick': function () { huePubSub.publish('editor.insert.at.cursor', { text: draggable }); } }"></div>
+              <!-- ko if: description -->
+              <div data-bind="html: descriptionMatch"></div>
+              <!-- /ko -->
+            </div>
+          <!-- /ko -->
         <!-- /ko -->
-      </div>
-      <!-- ko if: selectedFunction -->
-      <div class="assist-flex-half assist-function-details" data-bind="with: selectedFunction">
-        <div class="assist-panel-close"><button class="close" data-bind="click: function() { $parent.selectedFunction(null); }">&times;</button></div>
-        <div class="assist-function-signature blue" data-bind="draggableText: { text: draggable, meta: { type: 'function' } }, text: signature, event: { 'dblclick': function () { huePubSub.publish('editor.insert.at.cursor', { text: draggable }); } }"></div>
-        <!-- ko if: description -->
-        <div data-bind="html: descriptionMatch"></div>
-        <!-- /ko -->
-      </div>
       <!-- /ko -->
     </div>
   </div>
 `;
 
-class AssistFunctionsPanel {
-  constructor(params) {
-    this.categories = {};
-
-    this.connector = params.connector;
+class ConnectorUdfCategories {
+  constructor(connector) {
+    this.connector = connector;
+    this.loading = ko.observable(true);
+    this.label = this.connector.displayName;
 
-    this.activeDialect = ko.observable();
-    this.availableDialects = ko.observableArray();
+    this.categories = [];
 
     this.query = ko.observable().extend({ rateLimit: 400 });
-    this.selectedFunction = ko.observable();
 
-    const selectedFunctionPeDialect = {};
+    this.selectedFunction = ko.observable();
 
     this.selectedFunction.subscribe(newFunction => {
-      if (newFunction) {
-        selectedFunctionPeDialect[this.activeDialect()] = newFunction;
-        if (!newFunction.category.open()) {
-          newFunction.category.open(true);
-        }
+      if (newFunction && !newFunction.category.open()) {
+        newFunction.category.open(true);
       }
     });
 
-    this.activeCategories = ko.observableArray();
-
     this.filteredFunctions = ko.pureComputed(() => {
       const result = [];
+      if (this.loading()) {
+        return result;
+      }
       const lowerCaseQuery = this.query().toLowerCase();
       const replaceRegexp = new RegExp('(' + lowerCaseQuery + ')', 'i');
-      this.activeCategories().forEach(category => {
+      this.categories.forEach(category => {
         category.functions.forEach(fn => {
           if (fn.signature.toLowerCase().indexOf(lowerCaseQuery) === 0) {
             fn.weight = 2;
@@ -151,81 +161,13 @@ class AssistFunctionsPanel {
       return result;
     });
 
-    this.activeDialect.subscribe(newDialect => {
-      if (newDialect) {
-        this.selectedFunction(selectedFunctionPeDialect[newDialect]);
-        this.activeCategories(this.categories[newDialect]);
-        apiHelper.setInTotalStorage('assist', 'function.panel.active.dialect', newDialect);
-      }
+    this.fetchUdfs().then(() => {
+      this.loading(false);
     });
-
-    const updateDialect = dialect => {
-      this.availableDialects().every(availableDialect => {
-        if (availableDialect.toLowerCase() === dialect) {
-          if (this.activeDialect() !== availableDialect) {
-            this.activeDialect(availableDialect);
-          }
-          return false;
-        }
-        return true;
-      });
-    };
-
-    this.connector.subscribe(connector => {
-      if (connector) {
-        updateDialect(connector.dialect);
-      }
-    });
-
-    const configUpdated = () => {
-      const lastActiveDialect =
-        this.activeDialect() ||
-        apiHelper.getFromTotalStorage('assist', 'function.panel.active.dialect');
-
-      const uniqueDialects = {};
-
-      const configuredDialects = filterEditorConnectors(connector => {
-        const isMatch =
-          !uniqueDialects[connector.dialect] &&
-          (connector.dialect === 'hive' ||
-            connector.dialect === 'impala' ||
-            connector.dialect === 'pig');
-        uniqueDialects[connector.dialect] = true;
-        return isMatch;
-      }).map(connector => connector.dialect);
-      configuredDialects.sort();
-      this.availableDialects(configuredDialects);
-
-      this.availableDialects().forEach(dialect => {
-        this.initFunctions(dialect);
-      });
-
-      if (
-        lastActiveDialect &&
-        this.availableDialects().find(dialect => dialect === lastActiveDialect)
-      ) {
-        this.activeDialect(lastActiveDialect);
-      } else {
-        this.activeDialect(
-          this.availableDialects().length ? this.availableDialects()[0] : undefined
-        );
-      }
-      if (this.connector()) {
-        updateDialect(this.connector().dialect);
-      }
-    };
-
-    configUpdated();
-    huePubSub.subscribe(CONFIG_REFRESHED_EVENT, configUpdated);
   }
 
-  initFunctions(dialect) {
-    this.categories[dialect] = [];
-    const functions =
-      dialect === 'pig'
-        ? PigFunctions.CATEGORIZED_FUNCTIONS
-        : SqlFunctions.CATEGORIZED_FUNCTIONS[dialect.toLowerCase()];
-
+  async fetchUdfs() {
+    const functions = await getUdfCategories(this.connector);
     functions.forEach(category => {
       const koCategory = {
         name: category.name,
@@ -244,11 +186,80 @@ class AssistFunctionsPanel {
       koCategory.functions.forEach(fn => {
         fn.category = koCategory;
       });
-      this.categories[dialect].push(koCategory);
+      this.categories.push(koCategory);
     });
   }
 }
 
+class AssistFunctionsPanel {
+  constructor(params) {
+    this.categories = {};
+
+    this.activeConnector = params.activeConnector;
+
+    this.availableConnectorUdfs = ko.observableArray();
+    this.activeConnectorUdfs = ko.observable();
+
+    this.activeConnectorUdfs.subscribe(activeConnectorUdfs => {
+      apiHelper.setInTotalStorage(
+        'assist',
+        'function.panel.active.connector.type',
+        activeConnectorUdfs.connector.type
+      );
+    });
+
+    const setCategoriesFromConnector = connector => {
+      this.availableConnectorUdfs().some(availableConnectorUdfs => {
+        if (availableConnectorUdfs.connector.type === connector.type) {
+          this.activeConnectorUdfs(availableConnectorUdfs);
+          return true;
+        }
+      });
+    };
+
+    this.activeConnector.subscribe(connector => {
+      if (connector) {
+        setCategoriesFromConnector(connector);
+      }
+    });
+
+    const configUpdated = () => {
+      const lastActiveConnectorType =
+        (this.activeConnector() && this.activeConnector().type) ||
+        apiHelper.getFromTotalStorage('assist', 'function.panel.active.connector.type');
+
+      const availableConnectorUdfs = filterEditorConnectors(hasUdfCategories).map(
+        connector => new ConnectorUdfCategories(connector)
+      );
+
+      availableConnectorUdfs.sort((a, b) =>
+        a.connector.displayName.localeCompare(b.connector.displayName)
+      );
+
+      this.availableConnectorUdfs(availableConnectorUdfs);
+
+      let activeConnectorUdfs;
+
+      if (lastActiveConnectorType) {
+        activeConnectorUdfs = this.availableConnectorUdfs().find(
+          connectorUdf => connectorUdf.connector.type === lastActiveConnectorType
+        );
+      }
+
+      if (!activeConnectorUdfs && this.availableConnectorUdfs().length) {
+        activeConnectorUdfs = this.availableConnectorUdfs()[0];
+      }
+
+      this.activeConnectorUdfs(activeConnectorUdfs);
+
+      setCategoriesFromConnector(this.activeConnector());
+    };
+
+    configUpdated();
+    huePubSub.subscribe(CONFIG_REFRESHED_EVENT, configUpdated);
+  }
+}
+
 componentUtils.registerStaticComponent(NAME, AssistFunctionsPanel, TEMPLATE);
 
 export default AssistFunctionsPanel;

+ 17 - 9
desktop/core/src/desktop/js/ko/components/assist/ko.assistFunctionsPanel.test.js

@@ -30,10 +30,9 @@ describe('ko.assistFunctionsPanel.js', () => {
           app_config: {
             editor: {
               interpreters: [
-                { dialect: 'pig' },
-                { dialect: 'pig' },
-                { dialect: 'impala' },
-                { dialect: 'banana' }
+                { type: 'pig', dialect: 'pig', displayName: 'Pig' },
+                { type: 'impala', dialect: 'impala', displayName: 'Impala' },
+                { type: 'banana', dialect: 'banana', displayName: 'Banana' }
               ]
             }
           }
@@ -42,11 +41,19 @@ describe('ko.assistFunctionsPanel.js', () => {
     );
     await refreshConfig();
     const connector = ko.observable({ dialect: 'impala' });
-    const subject = new AssistFunctionsPanel({ connector: connector });
+    const subject = new AssistFunctionsPanel({ activeConnector: connector });
     await sleep(0);
 
     expect(spy).toHaveBeenCalled();
-    expect(subject.availableDialects()).toEqual(['impala', 'pig']);
+    expect(subject.availableConnectorUdfs().length).toEqual(2);
+    expect(
+      subject
+        .availableConnectorUdfs()
+        .every(
+          connectorUdfs =>
+            connectorUdfs.connector.type === 'pig' || connectorUdfs.connector.type === 'impala'
+        )
+    ).toBeTruthy();
 
     spy.mockRestore();
 
@@ -56,7 +63,7 @@ describe('ko.assistFunctionsPanel.js', () => {
           status: 0,
           app_config: {
             editor: {
-              interpreters: [{ dialect: 'pig' }]
+              interpreters: [{ type: 'pig', dialect: 'pig', displayName: 'Pig' }]
             }
           }
         })
@@ -68,7 +75,8 @@ describe('ko.assistFunctionsPanel.js', () => {
 
     await sleep(0);
 
-    expect(subject.availableDialects()).toEqual(['pig']);
-    expect(subject.activeDialect()).toEqual('pig');
+    expect(subject.availableConnectorUdfs().length).toEqual(1);
+    expect(subject.availableConnectorUdfs()[0].connector.type).toEqual('pig');
+    expect(subject.activeConnectorUdfs()).toEqual(subject.availableConnectorUdfs()[0]);
   });
 });

+ 2 - 2
desktop/core/src/desktop/js/ko/components/assist/ko.rightAssistPanel.js

@@ -75,10 +75,10 @@ const TEMPLATE = `
 
     <!-- ko if: functionsTabAvailable -->
     <div data-bind="
-        component: { 
+        component: {
           name: 'assist-functions-panel',
           params: {
-            connector: connector
+            activeConnector: connector
           }
         },
         visible: activeTab() === 'functions'

+ 2 - 2
desktop/core/src/desktop/js/ko/components/contextPopover/ko.quickQueryContext.js

@@ -18,7 +18,7 @@ import * as ko from 'knockout';
 
 import { MULTI_NAME as SIMPLE_ACE_MULTI } from 'ko/components/simpleAceEditor/ko.simpleAceEditor';
 import { CONTEXT_SELECTOR_COMPONENT } from 'ko/components/ko.contextSelector';
-import { NAME as DROP_DOWN } from 'ko/components/ko.dropDown';
+import { HUE_DROP_DOWN_COMPONENT } from 'ko/components/ko.dropDown';
 import { NAME as EXECUTABLE_ACTIONS } from 'apps/notebook2/components/ko.executableActions';
 import { SIMPLE_RESULT_GRID_COMPONENT } from 'apps/notebook2/components/resultGrid/ko.simpleResultGrid';
 
@@ -36,7 +36,7 @@ const TEMPLATE = `
 <div class="context-popover-flex-fill" style="overflow: auto;">
   <div style="display: inline-block" data-bind="
     component: {
-      name: '${ DROP_DOWN }',
+      name: '${ HUE_DROP_DOWN_COMPONENT }',
       params: {
         value: connector,
         labelAttribute: 'displayName',

+ 2 - 2
desktop/core/src/desktop/js/ko/components/doc/ko.linkSharing.js

@@ -23,7 +23,7 @@ import DisposableComponent from 'ko/components/DisposableComponent';
 import { DOCUMENT_UPDATED_EVENT } from 'doc/hueDocument';
 import huePubSub from 'utils/huePubSub';
 
-import 'ko/components/ko.dropDown';
+import { HUE_DROP_DOWN_COMPONENT } from 'ko/components/ko.dropDown';
 
 export const NAME = 'link-sharing';
 
@@ -47,7 +47,7 @@ const TEMPLATE = `
     <div>
       ${ I18n('Any logged in user with the link can') }
       <div class="perm-selector" data-bind="component: {
-          name: 'hue-drop-down',
+          name: '${ HUE_DROP_DOWN_COMPONENT }',
           params: {
             value: selectedPerm,
             entries: availablePerms,

+ 2 - 2
desktop/core/src/desktop/js/ko/components/ko.dropDown.js

@@ -20,7 +20,7 @@ import * as ko from 'knockout';
 import componentUtils from './componentUtils';
 import I18n from 'utils/i18n';
 
-export const NAME = 'hue-drop-down';
+export const HUE_DROP_DOWN_COMPONENT = 'hue-drop-down';
 
 const TEMPLATE = `
   <!-- ko if: !menuOnly && (!dropDownVisible() || !searchable) -->
@@ -258,7 +258,7 @@ class HueDropDown {
 }
 
 componentUtils.registerComponent(
-  NAME,
+  HUE_DROP_DOWN_COMPONENT,
   {
     createViewModel: function(params, componentInfo) {
       return new HueDropDown(params, componentInfo.element);

+ 2 - 2
desktop/core/src/desktop/js/ko/components/ko.dropDown.test.js

@@ -17,13 +17,13 @@
 import * as ko from 'knockout';
 
 import { koSetup } from 'jest/koTestUtils';
-import { NAME } from './ko.dropDown';
+import { HUE_DROP_DOWN_COMPONENT } from './ko.dropDown';
 
 describe('ko.dropDown.js', () => {
   const setup = koSetup();
 
   it('should render component', async () => {
-    const element = await setup.renderComponent(NAME, {
+    const element = await setup.renderComponent(HUE_DROP_DOWN_COMPONENT, {
       value: ko.observable(),
       entries: ko.observableArray()
     });