Эх сурвалжийг харах

HUE-8893 [assist] Move the editor context panel to a webpack module

Johan Ahlen 6 жил өмнө
parent
commit
f6ab4505bb

+ 2 - 2
desktop/core/src/desktop/js/api/apiHelper.js

@@ -470,8 +470,8 @@ class ApiHelper {
 
   /**
    * @param {string} url
-   * @param {Object} data
-   * @param {Object} options
+   * @param {Object} [data]
+   * @param {Object} [options]
    * @param {function} [options.successCallback]
    * @param {function} [options.errorCallback]
    * @param {boolean} [options.silenceErrors]

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

@@ -17,6 +17,7 @@
 import $ from 'jquery';
 import ko from 'knockout';
 
+import apiHelper from 'api/apiHelper';
 import AssistDbEntry from 'ko/components/assist/assistDbEntry';
 import dataCatalog from 'catalog/dataCatalog';
 import huePubSub from 'utils/huePubSub';
@@ -144,7 +145,7 @@ class AssistDbNamespace {
           self.selectedDatabase().loadEntries();
         }
         if (!self.navigationSettings.rightAssist) {
-          window.apiHelper.setInTotalStorage(
+          apiHelper.setInTotalStorage(
             'assist_' + self.sourceType + '_' + self.namespace.id,
             'lastSelectedDb',
             self.selectedDatabase().catalogEntry.name
@@ -175,7 +176,7 @@ class AssistDbNamespace {
         self.selectedDatabaseChanged();
         return;
       }
-      const lastSelectedDb = window.apiHelper.getFromTotalStorage(
+      const lastSelectedDb = apiHelper.getFromTotalStorage(
         'assist_' + self.sourceType + '_' + self.namespace.id,
         'lastSelectedDb',
         'default'

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

@@ -110,7 +110,7 @@ class AssistStorageEntry {
   loadPreview() {
     const self = this;
     self.loading(true);
-    window.apiHelper
+    apiHelper
       .fetchStoragePreview({
         path: self.getHierarchy(),
         type: self.type,
@@ -343,7 +343,7 @@ class AssistStorageEntry {
         type: 'dir'
       },
       parent: null,
-      apiHelper: window.apiHelper
+      apiHelper: apiHelper
     });
 
     path = (typeMatch ? typeMatch[2] : path).replace(/(?:^\/)|(?:\/$)/g, '').split('/');

+ 752 - 0
desktop/core/src/desktop/js/ko/components/assist/ko.assistEditorContextPanel.js

@@ -0,0 +1,752 @@
+// 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 $ from 'jquery';
+import ko from 'knockout';
+
+import AssistDbSource from 'ko/components/assist/assistDbSource';
+import AssistDbEntry from 'ko/components/assist/assistDbEntry';
+import componentUtils from 'ko/components/componentUtils';
+import huePubSub from 'utils/huePubSub';
+import dataCatalog from 'catalog/dataCatalog';
+import I18n from 'utils/i18n';
+
+const TEMPLATE = `
+  <div class="assist-inner-panel assist-assistant-panel">
+    <div class="assist-flex-panel">
+
+      <div class="assist-flex-header">
+        <div class="assist-inner-header">
+          <!-- ko if: isSolr -->
+          ${I18n('Indexes')}
+          <!-- /ko -->
+          <!-- ko ifnot: isSolr -->
+          ${I18n('Tables')}
+          <!-- ko if: statementCount() > 1 -->
+          <div class="statement-count">${I18n(
+            'Statement'
+          )} <span data-bind="text: activeStatementIndex() + '/' + statementCount()"></span></div>
+          <!-- /ko -->
+          <!-- /ko -->
+        </div>
+      </div>
+      <div class="assist-flex-search" data-bind="visible: activeTables().length > 0">
+        <div class="assist-filter">
+          <!-- ko component: {
+            name: 'inline-autocomplete',
+            params: {
+              querySpec: filter.querySpec,
+              facets: ['type'],
+              knownFacetValues: isSolr() ? SOLR_ASSIST_KNOWN_FACET_VALUES : SQL_ASSIST_KNOWN_FACET_VALUES,
+              autocompleteFromEntries: $component.autocompleteFromEntries
+            }
+          } --><!-- /ko -->
+        </div>
+      </div>
+      <div class="assist-flex-fill assist-db-scrollable" data-bind="delayedOverflow">
+        <!-- ko if: filteredTables().length === 0 && (!filter.querySpec() || filter.querySpec().query === '') -->
+        <div class="assist-no-entries">
+          <!-- ko if: isSolr -->
+          ${I18n('No indexes selected.')}
+          <!-- /ko -->
+          <!-- ko ifnot: isSolr  -->
+          ${I18n('No tables identified.')}
+          <!-- /ko -->
+        </div>
+        <!-- /ko -->
+        <!-- ko if: filteredTables().length === 0 && filter.querySpec() && filter.querySpec().query !== '' && !someLoading() -->
+        <div class="assist-no-entries">${I18n('No entries found')}</div>
+        <!-- /ko -->
+        <!-- ko if: filteredTables().length > 0 -->
+        <ul class="database-tree assist-tables" data-bind="foreachVisible: { data: filteredTables, minHeight: 22, container: '.assist-db-scrollable', skipScrollEvent: true }">
+          <!-- ko if: hasErrors -->
+          <li class="assist-table hue-warning" data-bind="attr: { 'title': $parent.isSolr() ? '${I18n(
+            'Error loading index details.'
+          )}' : '${I18n('Error loading table details.')}'}">
+            <span class="assist-entry">
+              <i class="hue-warning fa fa-fw muted valign-middle fa-warning"></i>
+              <!-- ko with: catalogEntry -->
+              <!-- ko if: typeof reload !== 'undefined' -->
+              <span data-bind="text: getDisplayName()"></span> <a class="inactive-action" href="javascript: void(0);" data-bind="click: reload"><i class="fa fa-refresh" data-bind="css: { 'fa-spin': reloading }"></i></a>
+              <!-- /ko -->
+              <!-- /ko -->
+            </span>
+          </li>
+          <!-- /ko -->
+          <!-- ko ifnot: hasErrors -->
+          <!-- ko template: { if: catalogEntry.isTableOrView(), name: 'assist-table-entry' } --><!-- /ko -->
+          <!-- ko template: { if: catalogEntry.isField(), name: 'assist-column-entry-assistant' } --><!-- /ko -->
+          <!-- /ko -->
+        </ul>
+        <!-- /ko -->
+        <!-- ko hueSpinner: { spin: filter.querySpec() && filter.querySpec().query !== '' && someLoading(), inline: true,  center: true} --><!-- /ko -->
+      </div>
+
+      <!-- ko if: showRisks -->
+      <div class="assist-flex-header assist-divider"><div class="assist-inner-header">${I18n(
+        'Query Analysis'
+      )}</div></div>
+      <div class="assist-flex-third">
+        <!-- ko if: ! activeRisks().hints -->
+        <div class="assist-no-entries">${I18n(
+          'Select a query or start typing to get optimization hints.'
+        )}</div>
+        <!-- /ko -->
+        <!-- ko if: activeRisks().hints && activeRisks().hints.length === 0 -->
+        <div class="assist-no-entries">${I18n('No optimizations identified.')}</div>
+        <!-- /ko -->
+        <!-- ko if: activeRisks().hints && activeRisks().hints.length > 0 -->
+        <ul class="risk-list" data-bind="foreach: activeRisks().hints">
+          <li>
+            <div class="risk-list-title" data-bind="css: { 'risk-list-high' : risk === 'high', 'risk-list-normal':  risk !== 'high' }, tooltip: { title: risk + ' ' + riskTables }"><span data-bind="text: riskAnalysis"></span></div>
+            <div class="risk-list-description" data-bind="text: riskRecommendation"></div>
+            <div class="risk-quickfix" data-bind="visible: (riskId === 17 || riskId === 22) && $parent.activeEditor() && $parent.activeLocations()" style="display:none;">
+              <a href="javascript:void(0);" data-bind="click: function () { $parent.addFilter(riskId); hueAnalytics.convert('optimizer', 'addFilter/' + riskId); }">${I18n(
+                'Add filter'
+              )}</a>
+            </div>
+          </li>
+        </ul>
+        <!-- /ko -->
+        <!-- ko if: hasMissingRisks() -->
+        <div class="margin-top-20">
+          <!-- ko hueSpinner: { spin: uploadingTableStats, inline: true} --><!-- /ko -->
+          <!-- ko ifnot: uploadingTableStats -->
+          <a href="javascript:void(0)" data-bind="visible: activeTables().length > 0, click: function() { uploadTableStats(true) }, attr: { 'title': ('${I18n(
+            'Add table '
+          )}'  + (isMissingDDL() ? 'DDL' : '') + (isMissingDDL() && isMissingStats() ? ' ${I18n(
+  'and'
+)} ' : '') + (isMissingStats() ? 'stats' : '')) }">
+            <i class="fa fa-fw fa-plus-circle"></i> ${I18n('Improve Analysis')}
+          </a>
+          <!-- /ko -->
+        </div>
+        <!-- /ko -->
+      </div>
+      <!-- /ko -->
+    </div>
+  </div>
+`;
+
+class AssistantUtils {
+  static getFilteredTablesPureComputed(vm) {
+    const openedByFilter = [];
+    return ko.pureComputed(() => {
+      if (
+        !vm.filter ||
+        !vm.filter.querySpec() ||
+        ((!vm.filter.querySpec().facets ||
+          Object.keys(vm.filter.querySpec().facets).length === 0) &&
+          (!vm.filter.querySpec().text || vm.filter.querySpec().text.length === 0))
+      ) {
+        while (openedByFilter.length) {
+          openedByFilter.pop().open(false);
+        }
+        return vm.activeTables();
+      }
+
+      const facets = vm.filter.querySpec().facets;
+
+      const result = [];
+      vm.activeTables().forEach(entry => {
+        let facetMatch =
+          !facets || !facets['type'] || (!facets['type']['table'] && !facets['type']['view']);
+        if (!facetMatch && facets['type']['table']) {
+          facetMatch = entry.catalogEntry.isTable();
+        }
+        if (!facetMatch && facets['type']['view']) {
+          facetMatch = entry.catalogEntry.isView();
+        }
+
+        let textMatch = !vm.filter.querySpec().text || vm.filter.querySpec().text.length === 0;
+        if (!textMatch) {
+          const nameLower = entry.catalogEntry.name.toLowerCase();
+          textMatch = vm.filter
+            .querySpec()
+            .text.every(text => nameLower.indexOf(text.toLowerCase()) !== -1);
+        }
+        entry.filterColumnNames(!textMatch);
+        if ((facetMatch && textMatch) || entry.filteredEntries().length > 0) {
+          if (!entry.open()) {
+            entry.open(true);
+            openedByFilter.push(entry);
+          }
+          result.push(entry);
+        }
+      });
+      return result;
+    });
+  }
+}
+
+class AssistEditorContextPanel {
+  constructor(params) {
+    this.disposals = [];
+    this.isSolr = ko.observable(false);
+    this.activeTab = params.activeTab;
+
+    this.sourceType = ko.observable(params.sourceType());
+
+    this.showRisks = ko.pureComputed(
+      () =>
+        window.HAS_OPTIMIZER &&
+        !this.isSolr() &&
+        (this.sourceType() === 'impala' || this.sourceType() === 'hive')
+    );
+
+    const typeSub = huePubSub.subscribe('active.snippet.type.changed', details => {
+      this.sourceType(details.type);
+    });
+
+    this.disposals.push(() => {
+      typeSub.remove();
+    });
+
+    this.uploadingTableStats = ko.observable(false);
+    this.activeStatement = ko.observable();
+    this.activeTables = ko.observableArray();
+    this.activeRisks = ko.observable({});
+    this.activeEditor = ko.observable();
+    this.activeRisks.subscribe(() => {
+      if (this.isMissingDDL()) {
+        this.uploadTableStats(false);
+      }
+    });
+    this.activeLocations = ko.observable();
+    this.statementCount = ko.observable(0);
+    this.activeStatementIndex = ko.observable(0);
+
+    this.hasActiveRisks = ko.pureComputed(
+      () => this.activeRisks().hints && this.activeRisks().hints.length > 0
+    );
+
+    this.hasMissingRisks = ko.pureComputed(() => this.isMissingDDL() || this.isMissingStats());
+
+    this.isMissingDDL = ko.pureComputed(
+      () => this.activeRisks().noDDL && this.activeRisks().noDDL.length > 0
+    );
+
+    this.isMissingStats = ko.pureComputed(
+      () =>
+        window.AUTO_UPLOAD_OPTIMIZER_STATS &&
+        this.activeRisks().noStats &&
+        this.activeRisks().noStats.length > 0
+    );
+
+    this.someLoading = ko.pureComputed(() =>
+      this.activeTables().some(
+        table => table.loading() || (!table.hasEntries() && !table.hasErrors())
+      )
+    );
+
+    const createQualifiedIdentifier = (identifierChain, defaultDatabase) => {
+      if (identifierChain.length === 1) {
+        return defaultDatabase + '.' + identifierChain[0].name;
+      }
+      return identifierChain
+        .map(identifier => identifier.name)
+        .join('.')
+        .toLowerCase();
+    };
+
+    this.filter = {
+      querySpec: ko
+        .observable({
+          query: '',
+          facets: {},
+          text: []
+        })
+        .extend({ rateLimit: 300 })
+    };
+
+    this.filteredTables = AssistantUtils.getFilteredTablesPureComputed(this);
+
+    const navigationSettings = {
+      showStats: true,
+      rightAssist: true
+    };
+    const i18n = {};
+
+    const sources = {};
+
+    let loadEntriesTimeout = -1;
+    // This fetches the columns for each table synchronously with 2 second in between.
+    const loadEntries = currentCount => {
+      let count = currentCount || 0;
+      count++;
+      if (count > 10) {
+        return;
+      }
+      window.clearTimeout(loadEntriesTimeout);
+      if (this.activeTables().length === 1) {
+        this.activeTables()[0].open(true);
+      } else {
+        loadEntriesTimeout = window.setTimeout(() => {
+          this.activeTables().every(table => {
+            if (!table.loaded && !table.hasErrors() && !table.loading()) {
+              table.loadEntries(() => {
+                loadEntries(count);
+              });
+              return false;
+            }
+            return !table.loading();
+          });
+        }, 2000);
+      }
+    };
+
+    this.autocompleteFromEntries = (nonPartial, partial) => {
+      const added = {};
+      const result = [];
+      const partialLower = partial.toLowerCase();
+      this.filteredTables().forEach(table => {
+        if (
+          !added[table.catalogEntry.name] &&
+          table.catalogEntry.name.toLowerCase().indexOf(partialLower) === 0
+        ) {
+          added[table.catalogEntry.name] = true;
+          result.push(nonPartial + partial + table.catalogEntry.name.substring(partial.length));
+        }
+        table.entries().forEach(col => {
+          if (
+            !added[col.catalogEntry.name] &&
+            col.catalogEntry.name.toLowerCase().indexOf(partialLower) === 0
+          ) {
+            added[col.catalogEntry.name] = true;
+            result.push(nonPartial + partial + col.catalogEntry.name.substring(partial.length));
+          }
+        });
+      });
+      return result;
+    };
+
+    const activeTablesSub = this.activeTables.subscribe(loadEntries);
+    this.disposals.push(() => {
+      window.clearTimeout(loadEntriesTimeout);
+      activeTablesSub.dispose();
+    });
+
+    let updateOnVisible = false;
+
+    const runningPromises = [];
+
+    const handleLocationUpdate = activeLocations => {
+      while (runningPromises.length) {
+        const promise = runningPromises.pop();
+        if (promise.cancel) {
+          promise.cancel();
+        }
+      }
+      updateOnVisible = false;
+
+      if (!sources[activeLocations.type]) {
+        sources[activeLocations.type] = {
+          assistDbSource: new AssistDbSource({
+            i18n: i18n,
+            initialNamespace: activeLocations.namespace,
+            type: activeLocations.type,
+            name: activeLocations.type,
+            navigationSettings: navigationSettings
+          }),
+          databaseIndex: {},
+          activeTableIndex: {}
+        };
+      }
+
+      const assistDbSource = sources[activeLocations.type].assistDbSource;
+      const databaseIndex = sources[activeLocations.type].databaseIndex;
+      const activeTableIndex = sources[activeLocations.type].activeTableIndex;
+
+      if (!activeLocations) {
+        this.activeLocations(undefined);
+        return;
+      }
+      this.activeLocations(activeLocations);
+      this.statementCount(activeLocations.totalStatementCount);
+      this.activeStatementIndex(activeLocations.activeStatementIndex);
+
+      if (activeLocations.activeStatementLocations) {
+        let updateTables = false;
+        const tableQidIndex = {};
+        const ctes = {};
+        activeLocations.activeStatementLocations.forEach(location => {
+          if (location.type === 'alias' && location.source === 'cte') {
+            ctes[location.alias.toLowerCase()] = true;
+          }
+        });
+
+        activeLocations.activeStatementLocations.forEach(location => {
+          if (
+            location.type === 'table' &&
+            (location.identifierChain.length !== 1 ||
+              !ctes[location.identifierChain[0].name.toLowerCase()])
+          ) {
+            const tableDeferred = $.Deferred();
+            const dbDeferred = $.Deferred();
+            runningPromises.push(tableDeferred);
+            runningPromises.push(dbDeferred);
+
+            const qid = createQualifiedIdentifier(
+              location.identifierChain,
+              activeLocations.defaultDatabase
+            );
+            if (activeTableIndex[qid]) {
+              tableQidIndex[qid] = true;
+              tableDeferred.resolve(activeTableIndex[qid]);
+              dbDeferred.resolve(activeTableIndex[qid].parent);
+            } else {
+              let database =
+                location.identifierChain.length === 2
+                  ? location.identifierChain[0].name
+                  : activeLocations.defaultDatabase;
+              database = database.toLowerCase();
+              if (databaseIndex[database]) {
+                dbDeferred.resolve(databaseIndex[database]);
+              } else {
+                dataCatalog
+                  .getEntry({
+                    sourceType: activeLocations.type,
+                    namespace: activeLocations.namespace,
+                    compute: activeLocations.compute,
+                    path: [database],
+                    definition: { type: 'database' }
+                  })
+                  .done(catalogEntry => {
+                    databaseIndex[database] = new AssistDbEntry(
+                      catalogEntry,
+                      null,
+                      assistDbSource,
+                      this.filter,
+                      i18n,
+                      navigationSettings
+                    );
+                    updateTables = true;
+                    dbDeferred.resolve(databaseIndex[database]);
+                  })
+                  .fail(() => {
+                    dbDeferred.reject();
+                  });
+              }
+
+              dbDeferred
+                .done(dbEntry => {
+                  dbEntry.catalogEntry
+                    .getChildren({ silenceErrors: true })
+                    .done(tableEntries => {
+                      const tableName =
+                        location.identifierChain[location.identifierChain.length - 1].name;
+                      const found = tableEntries.some(tableEntry => {
+                        if (tableEntry.name === tableName) {
+                          const assistTableEntry = new AssistDbEntry(
+                            tableEntry,
+                            dbEntry,
+                            assistDbSource,
+                            this.filter,
+                            i18n,
+                            navigationSettings
+                          );
+                          activeTableIndex[
+                            createQualifiedIdentifier(
+                              location.identifierChain,
+                              activeLocations.defaultDatabase
+                            )
+                          ] = assistTableEntry;
+                          tableQidIndex[qid] = true;
+                          updateTables = true;
+                          tableDeferred.resolve(assistTableEntry);
+                          return true;
+                        }
+                      });
+
+                      if (!found) {
+                        const missingEntry = new AssistDbEntry(
+                          {
+                            path: [dbEntry.catalogEntry.name, tableName],
+                            name: tableName,
+                            isTableOrView: () => true,
+                            getType: () => 'table',
+                            hasPossibleChildren: () => true,
+                            getSourceMeta: () =>
+                              $.Deferred()
+                                .resolve({ notFound: true })
+                                .promise(),
+                            getDisplayName: () => dbEntry.catalogEntry.name + '.' + tableName,
+                            reloading: ko.observable(false),
+                            reload: function() {
+                              const self = this;
+                              if (self.reloading()) {
+                                return;
+                              }
+                              self.reloading(true);
+                              huePubSub.subscribeOnce('data.catalog.entry.refreshed', data => {
+                                data.entry.getSourceMeta({ silenceErrors: true }).always(() => {
+                                  self.reloading(false);
+                                });
+                              });
+                              dataCatalog
+                                .getEntry({
+                                  sourceType: activeLocations.type,
+                                  namespace: activeLocations.namespace,
+                                  compute: activeLocations.compute,
+                                  path: []
+                                })
+                                .done(sourceEntry => {
+                                  sourceEntry
+                                    .getChildren()
+                                    .done(dbEntries => {
+                                      let clearPromise;
+                                      // Clear the database first if it exists without cascade
+                                      const hasDb = dbEntries.some(dbEntry => {
+                                        if (
+                                          dbEntry.name.toLowerCase() === self.path[0].toLowerCase()
+                                        ) {
+                                          clearPromise = dbEntry.clearCache({
+                                            invalidate: 'invalidate',
+                                            cascade: false
+                                          });
+                                          return true;
+                                        }
+                                      });
+                                      if (!hasDb) {
+                                        // If the database is missing clear the source without cascade
+                                        clearPromise = sourceEntry.clearCache({
+                                          invalidate: 'invalidate',
+                                          cascade: false
+                                        });
+                                      }
+                                      clearPromise.fail(() => {
+                                        self.reloading(false);
+                                      });
+                                    })
+                                    .fail(() => {
+                                      self.reloading(false);
+                                    });
+                                })
+                                .fail(() => {
+                                  self.reloading(false);
+                                });
+                            }
+                          },
+                          dbEntry,
+                          assistDbSource,
+                          this.filter,
+                          i18n,
+                          navigationSettings
+                        );
+                        activeTableIndex[
+                          createQualifiedIdentifier(
+                            location.identifierChain,
+                            activeLocations.defaultDatabase
+                          )
+                        ] = missingEntry;
+                        tableQidIndex[qid] = true;
+                        updateTables = true;
+                        missingEntry.hasErrors(true);
+                        tableDeferred.resolve(missingEntry);
+                      }
+                    })
+                    .fail(tableDeferred.reject);
+                })
+                .fail(tableDeferred.reject);
+            }
+          }
+        });
+
+        $.when.apply($, runningPromises).always(() => {
+          runningPromises.length = 0;
+          Object.keys(activeTableIndex).forEach(key => {
+            if (!tableQidIndex[key]) {
+              delete activeTableIndex[key];
+              updateTables = true;
+            }
+          });
+
+          if (updateTables) {
+            const tables = [];
+            Object.keys(activeTableIndex).forEach(key => {
+              tables.push(activeTableIndex[key]);
+            });
+
+            tables.sort((a, b) => {
+              return a.catalogEntry.name.localeCompare(b.catalogEntry.name);
+            });
+            this.activeTables(tables);
+          }
+        });
+      }
+    };
+
+    const entryRefreshedSub = huePubSub.subscribe('data.catalog.entry.refreshed', details => {
+      const sourceType = details.entry.getSourceType();
+      if (sources[sourceType]) {
+        let completeRefresh = false;
+        if (details.entry.isSource()) {
+          sources[sourceType].databaseIndex = {};
+          sources[sourceType].activeTableIndex = {};
+          completeRefresh = true;
+        } else if (
+          details.entry.isDatabase() &&
+          sources[sourceType].databaseIndex[details.entry.name]
+        ) {
+          const dbEntry = sources[sourceType].databaseIndex[details.entry.name];
+          const activeTableIndex = sources[sourceType].activeTableIndex;
+          Object.keys(activeTableIndex).forEach(tableKey => {
+            const tableEntry = activeTableIndex[tableKey];
+            if (tableEntry.parent === dbEntry) {
+              delete activeTableIndex[tableKey];
+              completeRefresh = true;
+            }
+          });
+        } else if (details.entry.isTableOrView()) {
+          const activeTableIndex = sources[sourceType].activeTableIndex;
+          if (activeTableIndex[details.entry.getQualifiedPath()]) {
+            delete activeTableIndex[details.entry.getQualifiedPath()];
+            completeRefresh = true;
+          }
+        }
+        if (completeRefresh) {
+          handleLocationUpdate(this.activeLocations());
+        }
+      }
+    });
+
+    if (this.activeTab() === 'editorAssistant') {
+      huePubSub.publish('get.active.editor.locations', handleLocationUpdate);
+    } else {
+      updateOnVisible = true;
+    }
+
+    const activeTabSub = this.activeTab.subscribe(activeTab => {
+      if (activeTab === 'editorAssistant' && updateOnVisible) {
+        huePubSub.publish('get.active.editor.locations', handleLocationUpdate);
+      }
+    });
+
+    this.disposals.push(() => {
+      entryRefreshedSub.remove();
+      activeTabSub.dispose();
+    });
+
+    const activeLocationsSub = huePubSub.subscribe('editor.active.locations', activeLocations => {
+      if (this.activeTab() === 'editorAssistant') {
+        handleLocationUpdate(activeLocations);
+      } else {
+        updateOnVisible = true;
+      }
+    });
+
+    const activeRisksSub = huePubSub.subscribe('editor.active.risks', details => {
+      if (details.risks !== this.activeRisks()) {
+        this.activeRisks(details.risks);
+        this.activeEditor(details.editor);
+      }
+    });
+
+    huePubSub.publish('editor.get.active.risks', details => {
+      this.activeRisks(details.risks);
+      this.activeEditor(details.editor);
+    });
+
+    this.disposals.push(() => {
+      activeLocationsSub.remove();
+      activeRisksSub.remove();
+    });
+  }
+
+  addFilter(riskId) {
+    if (this.activeLocations() && this.activeEditor()) {
+      this.activeLocations().activeStatementLocations.every(location => {
+        let isLowerCase = false;
+        if (
+          this.activeLocations().activeStatementLocations &&
+          this.activeLocations().activeStatementLocations.length > 0
+        ) {
+          const firstToken = this.activeLocations().activeStatementLocations[0].firstToken;
+          isLowerCase = firstToken === firstToken.toLowerCase();
+        }
+
+        if (
+          location.type === 'whereClause' &&
+          !location.subquery &&
+          (location.missing || riskId === 22)
+        ) {
+          this.activeEditor().moveCursorToPosition({
+            row: location.location.last_line - 1,
+            column: location.location.last_column - 1
+          });
+          this.activeEditor().clearSelection();
+
+          if (/\S$/.test(this.activeEditor().getTextBeforeCursor())) {
+            this.activeEditor().session.insert(this.activeEditor().getCursorPosition(), ' ');
+          }
+
+          const operation = location.missing ? 'WHERE ' : 'AND ';
+          this.activeEditor().session.insert(
+            this.activeEditor().getCursorPosition(),
+            isLowerCase ? operation.toLowerCase() : operation
+          );
+          this.activeEditor().focus();
+
+          if (riskId === 22) {
+            huePubSub.publish('editor.autocomplete.temporary.sort.override', {
+              partitionColumnsFirst: true
+            });
+          }
+
+          window.setTimeout(() => {
+            this.activeEditor().execCommand('startAutocomplete');
+          }, 1);
+
+          return false;
+        }
+        return true;
+      });
+    }
+  }
+
+  uploadTableStats(showProgress) {
+    if (this.uploadingTableStats()) {
+      return;
+    }
+    this.uploadingTableStats(true);
+    huePubSub.publish('editor.upload.table.stats', {
+      activeTables: this.activeTables(),
+      showProgress: showProgress,
+      callback: () => {
+        this.uploadingTableStats(false);
+      }
+    });
+  }
+
+  dispose() {
+    this.disposals.forEach(dispose => {
+      dispose();
+    });
+  }
+}
+
+let instance;
+
+const viewModelFactory = {
+  createViewModel: params => {
+    if (!instance) {
+      instance = new AssistEditorContextPanel(params);
+    }
+    return instance;
+  }
+};
+
+componentUtils.registerComponent('assist-editor-context-panel', viewModelFactory, TEMPLATE);

+ 140 - 238
desktop/core/src/desktop/js/ko/components/assist/ko.assistFunctionsPanel.js

@@ -14,36 +14,20 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import $ from 'jquery';
 import 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';
 
 const TEMPLATE = `
-  <script type="text/html" id="language-reference-topic-tree">
-    <!-- ko if: $data.length -->
-    <ul class="assist-docs-topic-tree " data-bind="foreach: $data">
-      <li>
-        <a class="black-link" href="javascript: void(0);" data-bind="click: function () { $component.selectedTopic($data); }, toggle: open">
-          <i class="fa fa-fw" style="font-size: 12px;" data-bind="css: { 'fa-chevron-right': children.length && !open(), 'fa-chevron-down': children.length && open() }"></i>
-          <span class="assist-field-link" href="javascript: void(0);" data-bind="css: { 'blue': $component.selectedTopic() === $data }, text: title"></span>
-        </a>
-        <!-- ko if: open -->
-        <!-- ko template: { name: 'language-reference-topic-tree', data: children } --><!-- /ko -->
-        <!-- /ko -->
-      </li>
-    </ul>
-    <!-- /ko -->
-  </script>
-
   <div class="assist-inner-panel">
     <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: sourceType, entries: availableTypes, linkTitle: '${I18n(
+          <div class="function-dialect-dropdown" data-bind="component: { name: 'hue-drop-down', params: { fixedPosition: true, value: activeType, entries: availableTypes, linkTitle: '${I18n(
             'Selected dialect'
           )}' } }" style="display: inline-block"></div>
         </div>
@@ -53,282 +37,200 @@ const TEMPLATE = `
           <input class="clearable" type="text" placeholder="Filter..." data-bind="clearable: query, value: query, valueUpdate: 'afterkeydown'">
         </div>
       </div>
-      <div class="assist-docs-topics" data-bind="css: { 'assist-flex-fill': !selectedTopic(), 'assist-flex-40': selectedTopic() }">
+      <div data-bind="css: { 'assist-flex-fill': !selectedFunction(), 'assist-flex-half': selectedFunction() }">
         <!-- ko ifnot: query -->
-        <!-- ko template: { name: 'language-reference-topic-tree', data: topics } --><!-- /ko -->
+        <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', draggable); } }, text: signature"></a>
+              </li>
+            </ul>
+          </li>
+        </ul>
         <!-- /ko -->
         <!-- ko if: query -->
-        <!-- ko if: filteredTopics().length > 0 -->
-        <ul class="assist-docs-topic-tree" data-bind="foreach: filteredTopics">
-          <li>
-            <a class="assist-field-link" href="javascript: void(0);" data-bind="css: { 'blue': $component.selectedTopic() === $data }, click: function () { $component.selectedTopic($data); }, html: titleMatch() || title"></a>
+        <!-- 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', draggable); } }, html: signatureMatch"></a>
           </li>
         </ul>
         <!-- /ko -->
-        <!-- ko if: filteredTopics().length === 0 -->
-        <ul class="assist-docs-topic-tree">
+        <!-- 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: selectedTopic -->
-      <div class="assist-flex-60 assist-docs-details" data-bind="with: selectedTopic">
-        <div class="assist-panel-close"><button class="close" data-bind="click: function() { $component.selectedTopic(undefined); }">&times;</button></div>
-        <div data-bind="html: bodyMatch() || body()"></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', draggable); } }"></div>
+        <!-- ko if: description -->
+        <div data-bind="html: descriptionMatch"></div>
+        <!-- /ko -->
       </div>
       <!-- /ko -->
     </div>
   </div>
 `;
 
-class LanguageReferenceTopic {
-  constructor(entry, index) {
-    this.ref = entry.ref;
-    this.title = entry.title;
-    this.index = index;
-    this.weight = 1;
-    this.children = [];
-    entry.children.forEach(child => {
-      this.children.push(new LanguageReferenceTopic(child, self.index));
-    });
-    this.loadDeferred = $.Deferred();
-    this.loading = ko.observable(false);
-    this.body = ko.observable();
-    this.bodyMatch = ko.observable();
-    this.open = ko.observable(false);
-    this.titleMatch = ko.observable();
-  }
-
-  load() {
-    if (this.body() || this.loading()) {
-      return this.loadDeferred.promise();
-    }
-    this.loading(true);
-    apiHelper
-      .simpleGet(this.index[this.ref])
-      .done(doc => {
-        this.body(doc.body);
-      })
-      .always(() => {
-        this.loading(false);
-        this.loadDeferred.resolve(this);
-      });
-    return this.loadDeferred.promise();
-  }
-}
-
-class LanguageReferencePanel {
-  constructor(params, element) {
+class AssistFunctionsPanel {
+  constructor() {
+    this.categories = {};
     this.disposals = [];
-    this.availableTypes = ['impala', 'hive'];
-    this.sourceType = ko.observable('hive');
-    this.allTopics = {
-      impala: [],
-      hive: []
-    };
 
-    window.IMPALA_DOC_TOP_LEVEL.forEach(topLevelItem => {
-      this.allTopics.impala.push(new LanguageReferenceTopic(topLevelItem, window.IMPALA_DOC_INDEX));
-    });
-    window.HIVE_DOC_TOP_LEVEL.forEach(topLevelItem => {
-      this.allTopics.hive.push(new LanguageReferenceTopic(topLevelItem, window.HIVE_DOC_INDEX));
-    });
+    this.activeType = ko.observable();
+    this.availableTypes = ko.observableArray(
+      window.IS_EMBEDDED ? ['Impala'] : ['Hive', 'Impala', 'Pig']
+    );
+    this.query = ko.observable().extend({ rateLimit: 400 });
+    this.selectedFunction = ko.observable();
 
-    const updateType = type => {
-      if (this.availableTypes.indexOf(type) !== -1) {
-        this.sourceType(type);
-      }
-    };
-
-    const activeSnippetTypeSub = huePubSub.subscribe('active.snippet.type.changed', details => {
-      updateType(details.type);
-    });
-    this.disposals.push(() => {
-      activeSnippetTypeSub.remove();
+    this.availableTypes().forEach(type => {
+      this.initFunctions(type);
     });
 
-    huePubSub.subscribeOnce('set.active.snippet.type', updateType);
-    huePubSub.publish('get.active.snippet.type');
-
-    this.topics = ko.pureComputed(() => this.allTopics[this.sourceType()]);
-
-    this.selectedTopic = ko.observable();
-
-    const selectedSub = this.selectedTopic.subscribe(newTopic => {
-      if (newTopic) {
-        newTopic.load();
+    const selectedFunctionPerType = { Hive: null, Impala: null, Pig: null };
+    this.selectedFunction.subscribe(newFunction => {
+      if (newFunction) {
+        selectedFunctionPerType[this.activeType()] = newFunction;
+        if (!newFunction.category.open()) {
+          newFunction.category.open(true);
+        }
       }
     });
-    this.disposals.push(() => {
-      selectedSub.dispose();
-    });
 
-    this.query = ko.observable().extend({ throttle: 200 });
-    this.filteredTopics = ko.observableArray();
+    this.activeCategories = ko.observableArray();
 
-    const sortFilteredTopics = () => {
-      this.filteredTopics.sort((a, b) => {
-        if (a.weight !== b.weight) {
-          return b.weight - a.weight;
-        }
-        return a.title.localeCompare(b.title);
-      });
-    };
-
-    this.query.subscribe(newVal => {
-      if (!newVal) {
-        return;
-      }
+    this.filteredFunctions = ko.pureComputed(() => {
+      const result = [];
       const lowerCaseQuery = this.query().toLowerCase();
       const replaceRegexp = new RegExp('(' + lowerCaseQuery + ')', 'i');
-      this.filteredTopics([]);
-
-      let sortTimeout = -1;
-
-      const findInside = topic => {
-        topic.load().done(loadedTopic => {
-          let match = false;
-          const titleIndex = loadedTopic.title.toLowerCase().indexOf(lowerCaseQuery);
-          if (titleIndex !== -1) {
-            loadedTopic.weight = titleIndex === 0 ? 2 : 1;
-            loadedTopic.titleMatch(
-              loadedTopic.title.replace(new RegExp('(' + lowerCaseQuery + ')', 'i'), '<b>$1</b>')
-            );
-            loadedTopic.bodyMatch(undefined);
-            this.filteredTopics.push(loadedTopic);
-            match = true;
+      this.activeCategories().forEach(category => {
+        category.functions.forEach(fn => {
+          if (fn.signature.toLowerCase().indexOf(lowerCaseQuery) === 0) {
+            fn.weight = 2;
+            fn.signatureMatch(fn.signature.replace(replaceRegexp, '<b>$1</b>'));
+            fn.descriptionMatch(fn.description);
+            result.push(fn);
+          } else if (fn.signature.toLowerCase().indexOf(lowerCaseQuery) !== -1) {
+            fn.weight = 1;
+            fn.signatureMatch(fn.signature.replace(replaceRegexp, '<b>$1</b>'));
+            fn.descriptionMatch(fn.description);
+            result.push(fn);
           } else if (
-            loadedTopic.body() &&
-            loadedTopic
-              .body()
-              .toLowerCase()
-              .indexOf(lowerCaseQuery) !== -1
+            fn.description &&
+            fn.description.toLowerCase().indexOf(lowerCaseQuery) !== -1
           ) {
-            loadedTopic.weight = 0;
-            loadedTopic.titleMatch(undefined);
-            loadedTopic.bodyMatch(loadedTopic.body().replace(replaceRegexp, '<b>$1</b>'));
-            this.filteredTopics.push(loadedTopic);
-            match = true;
+            fn.signatureMatch(fn.signature);
+            fn.descriptionMatch(fn.description.replace(replaceRegexp, '<b>$1</b>'));
+            fn.weight = 0;
+            result.push(fn);
           } else {
-            loadedTopic.titleMatch(undefined);
-            loadedTopic.bodyMatch(undefined);
-          }
-          if (match) {
-            window.clearTimeout(sortTimeout);
-            sortTimeout = window.setTimeout(sortFilteredTopics, 100);
+            if (fn.signatureMatch() !== fn.signature) {
+              fn.signatureMatch(fn.signature);
+            }
+            if (fn.descriptionMatch() !== fn.desciption) {
+              fn.descriptionMatch(fn.description);
+            }
           }
         });
-
-        topic.children.forEach(findInside);
-      };
-
-      this.topics.forEach(findInside);
-
-      window.setTimeout(() => {
-        // Initial sort deferred for promises to complete
-        sortFilteredTopics();
-      }, 0);
-    });
-
-    const selectedTopicSub = this.selectedTopic.subscribe(() => {
-      $(element)
-        .find('.assist-docs-details')
-        .scrollTop(0);
+      });
+      result.sort((a, b) => {
+        if (a.weight !== b.weight) {
+          return b.weight - a.weight;
+        }
+        return a.signature.localeCompare(b.signature);
+      });
+      return result;
     });
 
-    const querySub = this.query.subscribe(() => {
-      $(element)
-        .find('.assist-docs-topics')
-        .scrollTop(0);
+    this.activeType.subscribe(newType => {
+      this.selectedFunction(selectedFunctionPerType[newType]);
+      this.activeCategories(this.categories[newType]);
+      apiHelper.setInTotalStorage('assist', 'function.panel.active.type', newType);
     });
 
-    const scrollToSelectedTopic = function() {
-      const topics = $(element).find('.assist-docs-topics');
-      if (topics.find('.blue').length) {
-        topics.scrollTop(
-          Math.min(
-            topics.scrollTop() + topics.find('.blue').position().top - 20,
-            topics.find('> ul').height() - topics.height()
-          )
-        );
-      }
-    };
+    const lastActiveType = apiHelper.getFromTotalStorage(
+      'assist',
+      'function.panel.active.type',
+      this.availableTypes()[0]
+    );
+    this.activeType(lastActiveType);
 
-    const scrollToAnchor = function(anchorId) {
-      if (!anchorId) {
-        return;
-      }
-      const detailsPanel = $(element).find('.assist-docs-details');
-      const found = detailsPanel.find('#' + anchorId.split('/').join(' #'));
-      if (found.length) {
-        detailsPanel.scrollTop(found.position().top - 10);
-      }
-    };
-
-    huePubSub.subscribe('scroll.test', scrollToSelectedTopic);
-
-    const showTopicSub = huePubSub.subscribe('assist.lang.ref.panel.show.topic', targetTopic => {
-      const topicStack = [];
-      const findTopic = topics => {
-        topics.some(topic => {
-          topicStack.push(topic);
-          if (topic.ref === targetTopic.ref) {
-            while (topicStack.length) {
-              topicStack.pop().open(true);
-            }
-            this.query('');
-            this.selectedTopic(topic);
-            window.setTimeout(() => {
-              scrollToAnchor(targetTopic.anchorId);
-              scrollToSelectedTopic();
-            }, 0);
-            return true;
-          } else if (topic.children.length) {
-            const inChild = findTopic(topic.children);
-            if (inChild) {
-              return true;
-            }
+    const updateType = type => {
+      this.availableTypes().every(availableType => {
+        if (availableType.toLowerCase() === type) {
+          if (this.activeType() !== availableType) {
+            this.activeType(availableType);
           }
-          topicStack.pop();
-        });
-      };
-      findTopic(this.topics());
-    });
+          return false;
+        }
+        return true;
+      });
+    };
 
-    $(element).on('click.langref', event => {
-      if (event.target.className === 'hue-doc-internal-link') {
-        huePubSub.publish('assist.lang.ref.panel.show.topic', {
-          ref: $(event.target).data('doc-ref'),
-          anchorId: $(event.target).data('doc-anchor-id')
-        });
-      }
+    const activeSnippetTypeSub = huePubSub.subscribe('active.snippet.type.changed', details => {
+      updateType(details.type);
     });
 
     this.disposals.push(() => {
-      selectedTopicSub.dispose();
-      querySub.dispose();
-      showTopicSub.remove();
-      $(element).off('click.langref');
+      activeSnippetTypeSub.remove();
     });
+
+    huePubSub.subscribeOnce('set.active.snippet.type', updateType);
+    huePubSub.publish('get.active.snippet.type');
   }
 
   dispose() {
-    while (this.disposals.length) {
-      this.disposals.pop()();
-    }
+    this.disposals.forEach(dispose => {
+      dispose();
+    });
+  }
+
+  initFunctions(dialect) {
+    this.categories[dialect] = [];
+    const functions =
+      dialect === 'Pig'
+        ? PigFunctions.CATEGORIZED_FUNCTIONS
+        : SqlFunctions.CATEGORIZED_FUNCTIONS[dialect.toLowerCase()];
+
+    functions.forEach(category => {
+      const koCategory = {
+        name: category.name,
+        open: ko.observable(false),
+        functions: Object.keys(category.functions).map(key => {
+          const fn = category.functions[key];
+          return {
+            draggable: fn.draggable,
+            signature: fn.signature,
+            signatureMatch: ko.observable(fn.signature),
+            description: fn.description,
+            descriptionMatch: ko.observable(fn.description)
+          };
+        })
+      };
+      koCategory.functions.forEach(fn => {
+        fn.category = koCategory;
+      });
+      this.categories[dialect].push(koCategory);
+    });
   }
 }
 
 let instance;
 
 const viewModelFactory = {
-  createViewModel: (params, componentInfo) => {
+  createViewModel: () => {
     if (!instance) {
-      instance = new LanguageReferencePanel(params, componentInfo.element);
+      instance = new AssistFunctionsPanel();
     }
     return instance;
   }
 };
 
-componentUtils.registerComponent('language-reference-panel', viewModelFactory, TEMPLATE);
+componentUtils.registerComponent('assist-functions-panel', viewModelFactory, TEMPLATE);

+ 3 - 3
desktop/core/src/desktop/js/ko/components/assist/ko.assistLangRefPanel.js

@@ -118,7 +118,7 @@ class LanguageReferenceTopic {
   }
 }
 
-class LanguageReferencePanel {
+class AssistLangRefPanel {
   constructor(params, element) {
     this.disposals = [];
     this.availableTypes = ['impala', 'hive'];
@@ -325,10 +325,10 @@ let instance;
 const viewModelFactory = {
   createViewModel: (params, componentInfo) => {
     if (!instance) {
-      instance = new LanguageReferencePanel(params, componentInfo.element);
+      instance = new AssistLangRefPanel(params, componentInfo.element);
     }
     return instance;
   }
 };
 
-componentUtils.registerComponent('language-reference-panel', viewModelFactory, TEMPLATE);
+componentUtils.registerComponent('assist-language-reference-panel', viewModelFactory, TEMPLATE);

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

@@ -17,10 +17,11 @@
 import $ from 'jquery';
 import ko from 'knockout';
 
+import apiHelper from 'api/apiHelper';
+import AssistInnerPanel from 'ko/components/assist/assistInnerPanel';
 import componentUtils from 'ko/components/componentUtils';
 import huePubSub from 'utils/huePubSub';
 import I18n from 'utils/i18n';
-import AssistInnerPanel from 'ko/components/assist/assistInnerPanel';
 
 const TEMPLATE = `
   <script type="text/html" id="assist-panel-inner-header">
@@ -88,7 +89,7 @@ class AssistPanel {
     self.visiblePanel = ko.observable();
 
     self.lastOpenPanelType = ko.observable();
-    window.apiHelper.withTotalStorage('assist', 'last.open.panel', self.lastOpenPanelType);
+    apiHelper.withTotalStorage('assist', 'last.open.panel', self.lastOpenPanelType);
 
     huePubSub.subscribeOnce('cluster.config.set.config', clusterConfig => {
       if (clusterConfig && clusterConfig['app_config']) {

+ 3 - 0
desktop/core/src/desktop/js/ko/ko.all.js

@@ -129,9 +129,12 @@ import 'ko/bindings/ko.visibleOnHover';
 import 'ko/components/assist/ko.assistAdlsPanel';
 import 'ko/components/assist/ko.assistDbPanel';
 import 'ko/components/assist/ko.assistDocumentsPanel';
+import 'ko/components/assist/ko.assistEditorContextPanel';
+import 'ko/components/assist/ko.assistFunctionsPanel';
 import 'ko/components/assist/ko.assistGitPanel';
 import 'ko/components/assist/ko.assistHBasePanel';
 import 'ko/components/assist/ko.assistHdfsPanel';
+import 'ko/components/assist/ko.assistLangRefPanel';
 import 'ko/components/assist/ko.assistPanel';
 import 'ko/components/assist/ko.assistS3Panel';
 import 'ko/components/contextPopover/ko.contextPopover';

+ 4 - 624
desktop/core/src/desktop/templates/assist.mako

@@ -33,631 +33,12 @@ from desktop.views import _ko
 
 
 <%def name="assistPanel(is_s3_enabled=False)">
-
-
-  <script type="text/html" id="editor-assistant-panel-template">
-    <div class="assist-inner-panel assist-assistant-panel">
-      <div class="assist-flex-panel">
-
-        <div class="assist-flex-header">
-          <div class="assist-inner-header">
-            <!-- ko if: isSolr -->
-            ${ _('Indexes') }
-            <!-- /ko -->
-            <!-- ko ifnot: isSolr -->
-            ${ _('Tables') }
-            <!-- ko if: statementCount() > 1 -->
-            <div class="statement-count">${ _('Statement') } <span data-bind="text: activeStatementIndex() + '/' + statementCount()"></span></div>
-            <!-- /ko -->
-            <!-- /ko -->
-          </div>
-        </div>
-        <div class="assist-flex-search" data-bind="visible: activeTables().length > 0">
-          <div class="assist-filter">
-            <!-- ko component: {
-              name: 'inline-autocomplete',
-              params: {
-                querySpec: filter.querySpec,
-                facets: ['type'],
-                knownFacetValues: isSolr() ? SOLR_ASSIST_KNOWN_FACET_VALUES : SQL_ASSIST_KNOWN_FACET_VALUES,
-                autocompleteFromEntries: $component.autocompleteFromEntries
-              }
-            } --><!-- /ko -->
-          </div>
-        </div>
-        <div class="assist-flex-fill assist-db-scrollable" data-bind="delayedOverflow">
-          <!-- ko if: filteredTables().length === 0 && (!filter.querySpec() || filter.querySpec().query === '') -->
-          <div class="assist-no-entries">
-            <!-- ko if: isSolr -->
-            ${ _('No indexes selected.') }
-            <!-- /ko -->
-            <!-- ko ifnot: isSolr  -->
-            ${ _('No tables identified.') }
-            <!-- /ko -->
-          </div>
-          <!-- /ko -->
-          <!-- ko if: filteredTables().length === 0 && filter.querySpec() && filter.querySpec().query !== '' && !someLoading() -->
-          <div class="assist-no-entries">${ _('No entries found.') }</div>
-          <!-- /ko -->
-          <!-- ko if: filteredTables().length > 0 -->
-          <ul class="database-tree assist-tables" data-bind="foreachVisible: { data: filteredTables, minHeight: 22, container: '.assist-db-scrollable', skipScrollEvent: true }">
-            <!-- ko if: hasErrors -->
-            <li class="assist-table hue-warning" data-bind="attr: { 'title': $parent.isSolr() ? '${ _ko('Error loading index details.') }' : '${ _ko('Error loading table details.') }'}">
-              <span class="assist-entry">
-                <i class="hue-warning fa fa-fw muted valign-middle fa-warning"></i>
-                <!-- ko with: catalogEntry -->
-                <!-- ko if: typeof reload !== 'undefined' -->
-                <span data-bind="text: getDisplayName()"></span> <a class="inactive-action" href="javascript: void(0);" data-bind="click: reload"><i class="fa fa-refresh" data-bind="css: { 'fa-spin': reloading }"></i></a>
-                <!-- /ko -->
-                <!-- /ko -->
-              </span>
-            </li>
-            <!-- /ko -->
-            <!-- ko ifnot: hasErrors -->
-            <!-- ko template: { if: catalogEntry.isTableOrView(), name: 'assist-table-entry' } --><!-- /ko -->
-            <!-- ko template: { if: catalogEntry.isField(), name: 'assist-column-entry-assistant' } --><!-- /ko -->
-            <!-- /ko -->
-          </ul>
-          <!-- /ko -->
-          <!-- ko hueSpinner: { spin: filter.querySpec() && filter.querySpec().query !== '' && someLoading(), inline: true,  center: true} --><!-- /ko -->
-        </div>
-
-        <!-- ko if: showRisks -->
-        <div class="assist-flex-header assist-divider"><div class="assist-inner-header">${ _('Query Analysis') }</div></div>
-        <div class="assist-flex-third">
-          <!-- ko if: ! activeRisks().hints -->
-          <div class="assist-no-entries">${ _('Select a query or start typing to get optimization hints.') }</div>
-          <!-- /ko -->
-          <!-- ko if: activeRisks().hints && activeRisks().hints.length === 0 -->
-          <div class="assist-no-entries">${ _('No optimizations identified.') }</div>
-          <!-- /ko -->
-          <!-- ko if: activeRisks().hints && activeRisks().hints.length > 0 -->
-          <ul class="risk-list" data-bind="foreach: activeRisks().hints">
-            <li>
-              <div class="risk-list-title" data-bind="css: { 'risk-list-high' : risk === 'high', 'risk-list-normal':  risk !== 'high' }, tooltip: { title: risk + ' ' + riskTables }"><span data-bind="text: riskAnalysis"></span></div>
-              <div class="risk-list-description" data-bind="text: riskRecommendation"></div>
-              <div class="risk-quickfix" data-bind="visible: (riskId === 17 || riskId === 22) && $parent.activeEditor() && $parent.activeLocations()" style="display:none;">
-                <a href="javascript:void(0);" data-bind="click: function () { $parent.addFilter(riskId); hueAnalytics.convert('optimizer', 'addFilter/' + riskId); }">${ _('Add filter') }</a>
-              </div>
-            </li>
-          </ul>
-          <!-- /ko -->
-          <!-- ko if: hasMissingRisks() -->
-          <div class="margin-top-20">
-            <!-- ko hueSpinner: { spin: uploadingTableStats, inline: true} --><!-- /ko -->
-            <!-- ko ifnot: uploadingTableStats -->
-            <a href="javascript:void(0)" data-bind="visible: activeTables().length > 0, click: function() { uploadTableStats(true) }, attr: { 'title': ('${ _("Add table ") }'  + (isMissingDDL() ? 'DDL' : '') + (isMissingDDL() && isMissingStats() ? ' ${ _("and") } ' : '') + (isMissingStats() ? 'stats' : '')) }">
-              <i class="fa fa-fw fa-plus-circle"></i> ${_('Improve Analysis')}
-            </a>
-            <!-- /ko -->
-          </div>
-          <!-- /ko -->
-        </div>
-        <!-- /ko -->
-      </div>
-    </div>
-  </script>
-
+  
   <script type="text/javascript">
-    var AssistantUtils = (function () {
-      return {
-        getFilteredTablesPureComputed: function (vm) {
-          var openedByFilter = [];
-          return ko.pureComputed(function () {
-            if (vm.filter === null || !vm.filter.querySpec() || ((!vm.filter.querySpec().facets || Object.keys(vm.filter.querySpec().facets).length === 0) && (!vm.filter.querySpec().text || vm.filter.querySpec().text.length === 0))) {
-              while (openedByFilter.length) {
-                openedByFilter.pop().open(false);
-              }
-              return vm.activeTables();
-            }
-
-            var facets = vm.filter.querySpec().facets;
 
-            var result = [];
-            $.each(vm.activeTables(), function (index, entry) {
-              var facetMatch = !facets || !facets['type'] || (!facets['type']['table'] && !facets['type']['view']);
-              if (!facetMatch && facets['type']['table']) {
-                facetMatch = entry.catalogEntry.isTable();
-              }
-              if (!facetMatch && facets['type']['view']) {
-                facetMatch = entry.catalogEntry.isView();
-              }
-
-              var textMatch = !vm.filter.querySpec().text || vm.filter.querySpec().text.length === 0;
-              if (!textMatch) {
-                var nameLower = entry.catalogEntry.name.toLowerCase();
-                textMatch = vm.filter.querySpec().text.every(function (text) {
-                  return nameLower.indexOf(text.toLowerCase()) !== -1;
-                });
-              }
-              entry.filterColumnNames(!textMatch);
-              if ((facetMatch && textMatch) || entry.filteredEntries().length > 0) {
-                if (!entry.open()) {
-                  entry.open(true);
-                  openedByFilter.push(entry);
-                }
-                result.push(entry);
-              }
-            });
-            return result;
-          });
-        }
-      }
-    })();
 
     (function () {
-      function EditorAssistantPanel(params) {
-        var self = this;
-
-        self.disposals = [];
-        self.isSolr = ko.observable(false);
-        self.activeTab = params.activeTab;
-
-        self.sourceType = ko.observable(params.sourceType());
-
-        self.showRisks = ko.pureComputed(function () {
-          return window.HAS_OPTIMIZER && !self.isSolr() && (self.sourceType() === 'impala' || self.sourceType() === 'hive')
-        });
-
-        var typeSub = huePubSub.subscribe('active.snippet.type.changed', function (details) {
-          self.sourceType(details.type);
-        });
-
-        self.disposals.push(function () {
-          typeSub.remove();
-        });
-
-        self.uploadingTableStats = ko.observable(false);
-        self.activeStatement = ko.observable();
-        self.activeTables = ko.observableArray();
-        self.activeRisks = ko.observable({});
-        self.activeEditor = ko.observable();
-        self.activeRisks.subscribe(function() {
-          if (self.isMissingDDL()) {
-            self.uploadTableStats(false);
-          }
-        });
-        self.activeLocations = ko.observable();
-        self.statementCount = ko.observable(0);
-        self.activeStatementIndex = ko.observable(0);
-
-        self.hasActiveRisks = ko.pureComputed(function () {
-           return self.activeRisks().hints && self.activeRisks().hints.length > 0;
-        });
-
-        self.hasMissingRisks = ko.pureComputed(function () {
-          return self.isMissingDDL() || self.isMissingStats();
-        });
-
-        self.isMissingDDL = ko.pureComputed(function () {
-          return self.activeRisks().noDDL && self.activeRisks().noDDL.length > 0
-        });
-
-        self.isMissingStats = ko.pureComputed(function () {
-          % if OPTIMIZER.AUTO_UPLOAD_STATS.get():
-          return self.activeRisks().noStats && self.activeRisks().noStats.length > 0;
-          % else:
-          return false;
-          % endif
-        });
-
-        self.someLoading = ko.pureComputed(function () {
-          return self.activeTables().some(function (table) {
-            return table.loading() || (!table.hasEntries() && !table.hasErrors());
-          });
-        });
-
-        var createQualifiedIdentifier = function (identifierChain, defaultDatabase) {
-          if (identifierChain.length === 1) {
-            return defaultDatabase + '.' + identifierChain[0].name;
-          }
-          return $.map(identifierChain, function (identifier) {
-            return identifier.name;
-          }).join('.').toLowerCase();
-        };
-
-        self.filter = {
-          querySpec: ko.observable({
-            query: '',
-            facets: {},
-            text: []
-          }).extend({ rateLimit: 300 })
-        };
-
-        self.filteredTables = AssistantUtils.getFilteredTablesPureComputed(self);
-
-        var navigationSettings = {
-          showStats: true,
-          rightAssist: true
-        };
-        var i18n = {};
-
-        var sources = {};
-
-        var loadEntriesTimeout = -1;
-        // This fetches the columns for each table synchronously with 2 second in between.
-        var loadEntries = function (currentCount) {
-          var count = currentCount || 0;
-          count++;
-          if (count > 10) {
-            return;
-          }
-          window.clearTimeout(loadEntriesTimeout);
-          if (self.activeTables().length === 1) {
-            self.activeTables()[0].open(true);
-          } else {
-            loadEntriesTimeout = window.setTimeout(function () {
-              self.activeTables().every(function (table) {
-                if (!table.loaded && !table.hasErrors() && !table.loading()) {
-                  table.loadEntries(function () {
-                    loadEntries(count);
-                  });
-                  return false;
-                }
-                return !table.loading();
-              })
-            }, 2000);
-          }
-        };
-
-        self.autocompleteFromEntries = function (nonPartial, partial) {
-          var added = {};
-          var result = [];
-          var partialLower = partial.toLowerCase();
-          self.filteredTables().forEach(function (table) {
-            if (!added[table.catalogEntry.name] && table.catalogEntry.name.toLowerCase().indexOf(partialLower) === 0) {
-              added[table.catalogEntry.name] = true;
-              result.push(nonPartial + partial + table.catalogEntry.name.substring(partial.length))
-            }
-            table.entries().forEach(function (col) {
-              if (!added[col.catalogEntry.name] && col.catalogEntry.name.toLowerCase().indexOf(partialLower) === 0) {
-                added[col.catalogEntry.name] = true;
-                result.push(nonPartial + partial + col.catalogEntry.name.substring(partial.length))
-              }
-            })
-          });
-          return result;
-        };
-
-        var activeTablesSub = self.activeTables.subscribe(loadEntries);
-        self.disposals.push(function () {
-          window.clearTimeout(loadEntriesTimeout);
-          activeTablesSub.dispose();
-        });
-
-        var updateOnVisible = false;
-
-        var runningPromises = [];
-
-        var handleLocationUpdate = function (activeLocations) {
-          while (runningPromises.length) {
-            var promise = runningPromises.pop();
-            if (promise.cancel) {
-              promise.cancel();
-            }
-          }
-          updateOnVisible = false;
-
-          if (!sources[activeLocations.type]) {
-            sources[activeLocations.type] = {
-              assistDbSource: new AssistDbSource({
-                i18n: i18n,
-                initialNamespace: activeLocations.namespace,
-                type: activeLocations.type,
-                name: activeLocations.type,
-                navigationSettings: navigationSettings
-              }),
-              databaseIndex: {},
-              activeTableIndex: {}
-            }
-          }
-
-          var assistDbSource = sources[activeLocations.type].assistDbSource;
-          var databaseIndex = sources[activeLocations.type].databaseIndex;
-          var activeTableIndex = sources[activeLocations.type].activeTableIndex;
-
-          if (!activeLocations) {
-            self.activeLocations(undefined);
-            return;
-          }
-          self.activeLocations(activeLocations);
-          self.statementCount(activeLocations.totalStatementCount);
-          self.activeStatementIndex(activeLocations.activeStatementIndex);
-
-          if (activeLocations.activeStatementLocations) {
-            var updateTables = false;
-            var tableQidIndex = {};
-            var ctes = {};
-            activeLocations.activeStatementLocations.forEach(function (location) {
-              if (location.type === 'alias' && location.source === 'cte') {
-                ctes[location.alias.toLowerCase()] = true;
-              }
-            });
-
-            activeLocations.activeStatementLocations.forEach(function (location) {
-              if (location.type === 'table' && (location.identifierChain.length !== 1 || !ctes[location.identifierChain[0].name.toLowerCase()])) {
-                var tableDeferred = $.Deferred();
-                var dbDeferred = $.Deferred();
-                runningPromises.push(tableDeferred);
-                runningPromises.push(dbDeferred);
-
-                var qid = createQualifiedIdentifier(location.identifierChain, activeLocations.defaultDatabase);
-                if (activeTableIndex[qid]) {
-                  tableQidIndex[qid] = true;
-                  tableDeferred.resolve(activeTableIndex[qid]);
-                  dbDeferred.resolve(activeTableIndex[qid].parent);
-                } else {
-                  var database = location.identifierChain.length === 2 ? location.identifierChain[0].name : activeLocations.defaultDatabase;
-                  database = database.toLowerCase();
-                  if (databaseIndex[database]) {
-                    dbDeferred.resolve(databaseIndex[database]);
-                  } else {
-                    dataCatalog.getEntry({
-                      sourceType: activeLocations.type,
-                      namespace: activeLocations.namespace,
-                      compute: activeLocations.compute,
-                      path: [ database ],
-                      definition: { type: 'database' }
-                    }).done(function (catalogEntry) {
-                      databaseIndex[database] = new AssistDbEntry(catalogEntry, null, assistDbSource, self.filter, i18n,navigationSettings);
-                      updateTables = true;
-                      dbDeferred.resolve(databaseIndex[database])
-                    }).fail(function () {
-                      console.log('reject 1');
-                      dbDeferred.reject();
-                    });
-                  }
-
-                  dbDeferred.done(function (dbEntry) {
-                    dbEntry.catalogEntry.getChildren({ silenceErrors: true }).done(function (tableEntries) {
-                      var tableName = location.identifierChain[location.identifierChain.length - 1].name;
-                      var found = tableEntries.some(function (tableEntry) {
-                        if (tableEntry.name === tableName) {
-                          var assistTableEntry = new AssistDbEntry(
-                            tableEntry,
-                            dbEntry,
-                            assistDbSource,
-                            self.filter,
-                            i18n,
-                            navigationSettings
-                          );
-                          activeTableIndex[createQualifiedIdentifier(location.identifierChain, activeLocations.defaultDatabase)] = assistTableEntry;
-                          tableQidIndex[qid] = true;
-                          updateTables = true;
-                          tableDeferred.resolve(assistTableEntry);
-                          return true;
-                        }
-                      });
-
-                      if (!found) {
-                        var missingEntry = new AssistDbEntry(
-                          {
-                            path: [dbEntry.catalogEntry.name, tableName],
-                            name: tableName,
-                            isTableOrView: function () { return true; },
-                            getType: function () { return 'table' },
-                            hasPossibleChildren: function () { return true; },
-                            getSourceMeta: function () { return $.Deferred().resolve({ notFound: true }).promise() },
-                            getDisplayName: function () { return dbEntry.catalogEntry.name + '.' + tableName },
-                            reloading: ko.observable(false),
-                            reload: function () {
-                              var self = this;
-                              if (self.reloading()) {
-                                return;
-                              }
-                              self.reloading(true);
-                              huePubSub.subscribeOnce('data.catalog.entry.refreshed', function (data) {
-                                data.entry.getSourceMeta({ silenceErrors: true }).always(function () {
-                                  self.reloading(false)
-                                })
-                              });
-                              dataCatalog.getEntry({ sourceType: activeLocations.type, namespace: activeLocations.namespace, compute: activeLocations.compute, path: [] }).done(function (sourceEntry) {
-                                sourceEntry.getChildren().done(function (dbEntries) {
-                                  var clearPromise;
-                                   // Clear the database first if it exists without cascade
-                                  var hasDb = dbEntries.some(function (dbEntry) {
-                                    if (dbEntry.name.toLowerCase() === self.path[0].toLowerCase()) {
-                                      clearPromise = dbEntry.clearCache({ invalidate: 'invalidate', cascade: false });
-                                      return true;
-                                    }
-                                  });
-                                  if (!hasDb) {
-                                    // If the database is missing clear the source without cascade
-                                    clearPromise = sourceEntry.clearCache({ invalidate: 'invalidate', cascade: false });
-                                  }
-                                  clearPromise.fail(function () {
-                                    self.reloading(false);
-                                  });
-                                }).fail(function () {
-                                  self.reloading(false);
-                                })
-                              }).fail(function () {
-                                self.reloading(false);
-                              });
-                            }
-                          },
-                          dbEntry,
-                          assistDbSource,
-                          self.filter,
-                          i18n,
-                          navigationSettings
-                        );
-                        activeTableIndex[createQualifiedIdentifier(location.identifierChain, activeLocations.defaultDatabase)] = missingEntry;
-                        tableQidIndex[qid] = true;
-                        updateTables = true;
-                        missingEntry.hasErrors(true);
-                        tableDeferred.resolve(missingEntry);
-                      }
-                    }).fail(tableDeferred.reject);
-                  }).fail(tableDeferred.reject);
-                }
-              }
-            });
-
-            $.when.apply($, runningPromises).always(function () {
-              runningPromises.length = 0;
-              Object.keys(activeTableIndex).forEach(function (key) {
-                if (!tableQidIndex[key]) {
-                  delete activeTableIndex[key];
-                  updateTables = true;
-                }
-              });
-
-              if (updateTables) {
-                var tables = [];
-                Object.keys(activeTableIndex).forEach(function (key) {
-                  tables.push(activeTableIndex[key]);
-                });
-
-                tables.sort(function (a, b) {
-                  return a.catalogEntry.name.localeCompare(b.catalogEntry.name);
-                });
-                self.activeTables(tables);
-              }
-            });
-          }
-        };
-
-        var entryRefreshedSub = huePubSub.subscribe('data.catalog.entry.refreshed', function (details) {
-          var sourceType = details.entry.getSourceType();
-          if (sources[sourceType]) {
-            var completeRefresh = false;
-            if (details.entry.isSource()) {
-              sources[sourceType].databaseIndex = {};
-              sources[sourceType].activeTableIndex = {};
-              completeRefresh = true;
-            } else if (details.entry.isDatabase() && sources[sourceType].databaseIndex[details.entry.name]) {
-              var dbEntry = sources[sourceType].databaseIndex[details.entry.name];
-              var activeTableIndex = sources[sourceType].activeTableIndex;
-              Object.keys(activeTableIndex).forEach(function (tableKey) {
-                var tableEntry = activeTableIndex[tableKey];
-                if (tableEntry.parent === dbEntry) {
-                  delete activeTableIndex[tableKey];
-                  completeRefresh = true;
-                }
-              });
-            } else if (details.entry.isTableOrView()) {
-              var activeTableIndex = sources[sourceType].activeTableIndex;
-              if (activeTableIndex[details.entry.getQualifiedPath()]) {
-                delete activeTableIndex[details.entry.getQualifiedPath()];
-                completeRefresh = true;
-              }
-            }
-            if (completeRefresh) {
-              handleLocationUpdate(self.activeLocations());
-            }
-          }
-        });
-
-        if (self.activeTab() === 'editorAssistant') {
-          huePubSub.publish('get.active.editor.locations', handleLocationUpdate);
-        } else {
-          updateOnVisible = true;
-        }
 
-        var activeTabSub = self.activeTab.subscribe(function (activeTab) {
-          if (activeTab === 'editorAssistant' && updateOnVisible) {
-            huePubSub.publish('get.active.editor.locations', handleLocationUpdate);
-          }
-        });
-
-        self.disposals.push(function () {
-          entryRefreshedSub.remove();
-          activeTabSub.dispose();
-        });
-
-        var activeLocationsSub = huePubSub.subscribe('editor.active.locations', function (activeLocations) {
-          if (self.activeTab() === 'editorAssistant') {
-            handleLocationUpdate(activeLocations);
-          } else {
-            updateOnVisible = true;
-          }
-        });
-
-        var activeRisksSub = huePubSub.subscribe('editor.active.risks', function (details) {
-          if (details.risks !== self.activeRisks()) {
-            self.activeRisks(details.risks);
-            self.activeEditor(details.editor);
-          }
-        });
-
-        huePubSub.publish('editor.get.active.risks', function (details) {
-          self.activeRisks(details.risks);
-          self.activeEditor(details.editor);
-        });
-
-        self.disposals.push(function () {
-          activeLocationsSub.remove();
-          activeRisksSub.remove();
-        });
-      }
-
-      EditorAssistantPanel.prototype.addFilter = function (riskId) {
-        var self = this;
-        if (self.activeLocations() && self.activeEditor()) {
-          self.activeLocations().activeStatementLocations.every(function (location) {
-            var isLowerCase = false;
-            if (self.activeLocations().activeStatementLocations && self.activeLocations().activeStatementLocations.length > 0) {
-              var firstToken = self.activeLocations().activeStatementLocations[0].firstToken;
-              isLowerCase = firstToken === firstToken.toLowerCase();
-            }
-
-            if (location.type === 'whereClause' && !location.subquery && (location.missing || riskId === 22 )) {
-              self.activeEditor().moveCursorToPosition({
-                row: location.location.last_line - 1,
-                column: location.location.last_column - 1
-              });
-              self.activeEditor().clearSelection();
-
-              if (/\S$/.test(self.activeEditor().getTextBeforeCursor())) {
-                self.activeEditor().session.insert(self.activeEditor().getCursorPosition(), ' ');
-              }
-
-              var operation = location.missing ? 'WHERE ' : 'AND ';
-              self.activeEditor().session.insert(self.activeEditor().getCursorPosition(), isLowerCase ? operation.toLowerCase() : operation);
-              self.activeEditor().focus();
-
-              if (riskId === 22) {
-                huePubSub.publish('editor.autocomplete.temporary.sort.override', { partitionColumnsFirst: true });
-              }
-
-              window.setTimeout(function () {
-                self.activeEditor().execCommand("startAutocomplete");
-              }, 1);
-
-              return false;
-            }
-            return true;
-          })
-        }
-      };
-
-      EditorAssistantPanel.prototype.uploadTableStats = function (showProgress) {
-        var self = this;
-        if (self.uploadingTableStats()) {
-          return;
-        }
-        self.uploadingTableStats(true);
-        huePubSub.publish('editor.upload.table.stats', {
-          activeTables: self.activeTables(),
-          showProgress: showProgress,
-          callback: function () {
-            self.uploadingTableStats(false);
-          }
-        });
-      };
-
-      EditorAssistantPanel.prototype.dispose = function () {
-        var self = this;
-        self.disposals.forEach(function (dispose) {
-          dispose();
-        })
-      };
-
-      ko.components.register('editor-assistant-panel', {
-        viewModel: EditorAssistantPanel,
-        template: { element: 'editor-assistant-panel-template' }
-      });
     })();
   </script>
 
@@ -907,15 +288,15 @@ from desktop.views import _ko
     <!-- ko if: visible -->
     <div class="right-assist-contents">
       <!-- ko if: editorAssistantTabAvailable-->
-      <div data-bind="component: { name: 'editor-assistant-panel', params: { activeTab: activeTab, sourceType: sourceType } }, visible: activeTab() === 'editorAssistant'"></div>
+      <div data-bind="component: { name: 'assist-editor-context-panel', params: { activeTab: activeTab, sourceType: sourceType } }, visible: activeTab() === 'editorAssistant'"></div>
       <!-- /ko -->
 
       <!-- ko if: functionsTabAvailable -->
-      <div data-bind="component: { name: 'functions-panel' }, visible: activeTab() === 'functions'"></div>
+      <div data-bind="component: { name: 'assist-functions-panel' }, visible: activeTab() === 'functions'"></div>
       <!-- /ko -->
 
       <!-- ko if: langRefTabAvailable -->
-      <div data-bind="component: { name: 'language-reference-panel' }, visible: activeTab() === 'langRef'"></div>
+      <div data-bind="component: { name: 'assist-language-reference-panel' }, visible: activeTab() === 'langRef'"></div>
       <!-- /ko -->
 
       <!-- ko if: dashboardAssistantTabAvailable -->
@@ -928,7 +309,6 @@ from desktop.views import _ko
     <!-- /ko -->
   </script>
 
-
   <script type="text/javascript">
     (function () {
 

+ 12 - 0
desktop/core/src/desktop/templates/global_js_constants.mako

@@ -79,6 +79,7 @@
   window.HAS_READ_ONLY_CATALOG = '${ has_readonly_catalog(request.user) }' === 'True' || '${ has_write_access(request.user) }' === 'False';
 
   window.HAS_OPTIMIZER = '${ has_optimizer() }' === 'True';
+  window.AUTO_UPLOAD_OPTIMIZER_STATS = '${ OPTIMIZER.AUTO_UPLOAD_STATS.get() }' === 'True';
 
   ## In the past was has_workload_analytics()
   window.HAS_WORKLOAD_ANALYTICS = '${ ENABLE_QUERY_ANALYSIS.get() }' === 'True';
@@ -117,9 +118,11 @@
     'Active database': '${ _('Active database') }',
     'Active namespace': '${ _('Active namespace') }',
     'Add a description...': '${ _('Add a description...') }',
+    'Add filter': '${ _('Add filter') }',
     'Add more...': '${ _('Add more...') }',
     'Add privilege': '${ _('Add privilege') }',
     'Add properties...': '${ _('Add properties...') }',
+    'Add table': '${ _('Add table') }',
     'Add tags...': '${ _('Add tags...') }',
     'Add': '${ _('Add') }',
     'Admin': '${ _('Admin') }',
@@ -204,8 +207,10 @@
     'Error loading databases.': '${ _('Error loading databases.') }',
     'Error loading entries': '${ _('Error loading entries') }',
     'Error loading fields.': '${ _('Error loading fields.') }',
+    'Error loading index details.': '${ _('Error loading index details.') }',
     'Error loading namespaces.': '${ _('Error loading namespaces.') }',
     'Error loading samples': '${ _('Error loading samples') }',
+    'Error loading table details.': '${ _('Error loading table details.') }',
     'Error loading tables.': '${ _('Error loading tables.') }',
     'Error while copying results.': '${ _('Error while copying results.') }',
     'Example: SELECT * FROM tablename, or press CTRL + space': '${ _('Example: SELECT * FROM tablename, or press CTRL + space') }',
@@ -239,6 +244,7 @@
     'Ignore this type of error': '${_('Ignore this type of error')}',
     'Impala Query': '${_('Impala Query')}',
     'Import Job': '${_('Import Job')}',
+    'Improve Analysis': '${_('Improve Analysis')}',
     'Indexes': '${ _('Indexes') }',
     'Insert at cursor': '${_('Insert at cursor')}',
     'Insert in the editor': '${ _('Insert in the editor') }',
@@ -286,15 +292,18 @@
     'No databases found.': '${ _('No databases found.') }',
     'No documents found': '${ _('No documents found') }',
     'No entries found': '${ _('No entries found') }',
+    'No indexes selected.': '${ _('No indexes selected.') }',
     'No logs available at this moment.': '${ _('No logs available at this moment.') }',
     'No match found': '${ _('No match found') }',
     'No namespaces found': '${ _('No namespaces found') }',
     'No namespaces found.': '${ _('No namespaces found.') }',
+    'No optimizations identified.': '${ _('No optimizations identified.') }',
     'No privileges found for the selected object.': '${ _('No privileges found for the selected object.') }',
     'No results found': '${_('No results found')}',
     'No results found.': '${_('No results found.')}',
     'No tables available.': '${ _('No tables available.') }',
     'No tables found': '${ _('No tables found') }',
+    'No tables identified.': '${ _('No tables identified.') }',
     'No task history.': '${_('Not task history.')}',
     'No': '${ _('No') }',
     'No related computes': '${_('No related computes')}',
@@ -337,6 +346,7 @@
     'Primary key': '${ _('Primary key') }',
     'Project': '${ _('Project') }',
     'Queries': '${ _('Queries') }',
+    'Query Analysis': '${ _('Query Analysis') }',
     'Query browser': '${ _('Query browser') }',
     'Query failed': '${ _('Query failed') }',
     'Query requires a select or aggregate.': '${ _('Query requires a select or aggregate.') }',
@@ -358,6 +368,7 @@
     'Search Dashboard': '${_('Search Dashboard')}',
     'Search data and saved documents...': '${ _('Search data and saved documents...') }',
     'Search saved documents...': '${_('Search saved documents...')}',
+    'Select a query or start typing to get optimization hints.': '${_('Select a query or start typing to get optimization hints')}',
     'Select this folder': '${_('Select this folder')}',
     'Selected dialect': '${_('Selected dialect')}',
     'Selected entry': '${_('Selected entry')}',
@@ -377,6 +388,7 @@
     'Size': '${ _('Size') }',
     'Solr Search': '${ _('Solr Search') }',
     'Sources': '${ _('Sources') }',
+    'Statement': '${ _('Statement') }',
     'Spark Job': '${_('Spark Job')}',
     'Starting': '${_('Starting')}',
     'Stats': '${_('Stats')}',