Sfoglia il codice sorgente

[editor] Replace the editor_autocomplete_timeout setting with disable_source_autocomplete

Setting editor_autocomplete_timeout is currently dead code, so I decided to deprecate it with this commit and replace it with a boolean disable_source_autocomplete setting. The timeout setting was introduced in the past to support two use cases. First the old autocomplete selection panel would not show until all autocomplete suggestions had been fetched, to speed things up in the UI this could be set to a few seconds, today this is no longer needed as results are populated as they come in from the backend. The second use case was to disable API-based autocomplete all together by setting it 0, this has been broken for at least 3 years now.

Given that the timeout is no longer necessary I went for a boolean replacement.
Johan Åhlén 1 anno fa
parent
commit
1291cc81c1
25 ha cambiato i file con 721 aggiunte e 402 eliminazioni
  1. 0 1
      .eslintrc.js
  2. 0 1
      apps/beeswax/src/beeswax/templates/execute.mako
  3. 5 3
      desktop/conf.dist/hue.ini
  4. 5 3
      desktop/conf/pseudo-distributed.ini.tmpl
  5. 5 6
      desktop/core/src/desktop/conf.py
  6. 0 1
      desktop/core/src/desktop/js/apps/editor/EditorViewModel.js
  7. 306 0
      desktop/core/src/desktop/js/apps/editor/components/aceEditor/autocomplete/AutocompleteResults.test.ts
  8. 39 29
      desktop/core/src/desktop/js/apps/editor/components/aceEditor/autocomplete/AutocompleteResults.ts
  9. 1 2
      desktop/core/src/desktop/js/apps/editor/snippet.js
  10. 0 1
      desktop/core/src/desktop/js/apps/notebook/NotebookViewModel.js
  11. 1 4
      desktop/core/src/desktop/js/apps/notebook/aceAutocompleteWrapper.js
  12. 1 2
      desktop/core/src/desktop/js/apps/notebook/snippet.js
  13. 1 0
      desktop/core/src/desktop/js/config/types.ts
  14. 0 1
      desktop/core/src/desktop/js/jest/jest.init.js
  15. 36 29
      desktop/core/src/desktop/js/sql/autocompleteResults.js
  16. 316 303
      desktop/core/src/desktop/js/sql/autocompleteResults.test.js
  17. 0 2
      desktop/core/src/desktop/js/sql/sqlAutocompleter.test.ignore.js
  18. 0 1
      desktop/core/src/desktop/js/types/types.ts
  19. 0 3
      desktop/core/src/desktop/js/utils/hdfsAutocompleter.js
  20. 5 4
      desktop/core/src/desktop/models.py
  21. 0 2
      desktop/core/src/desktop/templates/common_header_m.mako
  22. 0 1
      desktop/core/src/desktop/templates/global_js_constants.mako
  23. 0 1
      desktop/libs/notebook/src/notebook/templates/editor2.mako
  24. 0 1
      desktop/libs/notebook/src/notebook/templates/editor_components.mako
  25. 0 1
      desktop/libs/notebook/src/notebook/templates/editor_m.mako

+ 0 - 1
.eslintrc.js

@@ -2,7 +2,6 @@ const normalGlobals = [];
 
 const hueGlobals = [
   // global_js_constants.mako
-  'AUTOCOMPLETE_TIMEOUT',
   'CACHEABLE_TTL',
   'CSRF_TOKEN',
   'DOCUMENT_TYPES',

+ 0 - 1
apps/beeswax/src/beeswax/templates/execute.mako

@@ -1173,7 +1173,6 @@ var autocompleter = new AceAutocompleteWrapper({
   user: HIVE_AUTOCOMPLETE_USER,
   oldEditor: true,
   optEnabled: false,
-  timeout: AUTOCOMPLETE_TIMEOUT
 });
 
 var truncateOutput = function (obj) {

+ 5 - 3
desktop/conf.dist/hue.ini

@@ -239,9 +239,11 @@ http_500_debug_mode=false
 # Choose whether to allow multi tenancy or not.
 ## enable_organizations=false
 
-# Editor autocomplete timeout (ms) when fetching columns, fields, tables etc.
-# To disable this type of autocompletion set the value to 0.
-## editor_autocomplete_timeout=30000
+# Choose whether the editor autocomplete should gather suggestions from external source or not. The editor
+# autocomplete uses various sources for its suggestions, listing databases, tables, columns files etc. The results are
+# cached on the client (see cacheable_ttl) so the calls are kept to a minimum but if you prefer to disable these calls
+# all together from the editor set this to true.
+## disable_source_autocomplete=false
 
 # Enable saved default configurations for Hive, Impala, Spark, and Oozie.
 ## use_default_configuration=false

+ 5 - 3
desktop/conf/pseudo-distributed.ini.tmpl

@@ -244,9 +244,11 @@
   # Choose whether to allow multi tenancy or not.
   ## enable_organizations=false
 
-  # Editor autocomplete timeout (ms) when fetching columns, fields, tables etc.
-  # To disable this type of autocompletion set the value to 0.
-  ## editor_autocomplete_timeout=30000
+  # Choose whether the editor autocomplete should gather suggestions from external source or not. The editor
+  # autocomplete uses various sources for its suggestions, listing databases, tables, columns files etc. The results are
+  # cached on the client (see cacheable_ttl) so the calls are kept to a minimum but if you prefer to disable these calls
+  # all together from the editor set this to true.
+  ## disable_source_autocomplete=false
 
   # Enable saved default configurations for Hive, Impala, Spark, and Oozie.
   ## use_default_configuration=false

+ 5 - 6
desktop/core/src/desktop/conf.py

@@ -1817,11 +1817,11 @@ ENABLE_SQL_SYNTAX_CHECK = Config(
   help=_('Choose whether to enable SQL syntax check or not.')
 )
 
-EDITOR_AUTOCOMPLETE_TIMEOUT = Config(
-  key='editor_autocomplete_timeout',
-  type=int,
-  default=30000,
-  help=_('Timeout value in ms for autocomplete of columns, tables, values etc. 0 = disabled.')
+DISABLE_SOURCE_AUTOCOMPLETE = Config(
+  key='disable_source_autocomplete',
+  type=coerce_bool,
+  default=False,
+  help=_('Choose whether the editor autocomplete should gather suggestions from external sources or not.')
 )
 
 ENABLE_HUE_5 = Config(
@@ -1948,7 +1948,6 @@ TRACING = ConfigSection(
     ),
 ))
 
-
 def task_server_default_result_directory():
   """Local directory to store task results."""
   return 'file://%s' % get_run_root('logs')

+ 0 - 1
desktop/core/src/desktop/js/apps/editor/EditorViewModel.js

@@ -108,7 +108,6 @@ export default class EditorViewModel {
       }
     });
 
-    this.autocompleteTimeout = options.autocompleteTimeout;
     this.lastNotifiedDialect = undefined;
 
     this.combinedContent = ko.observable();

+ 306 - 0
desktop/core/src/desktop/js/apps/editor/components/aceEditor/autocomplete/AutocompleteResults.test.ts

@@ -0,0 +1,306 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as Vue from 'vue';
+import * as CatalogApi from 'catalog/api';
+import { CancellablePromise } from 'api/cancellablePromise';
+import AutocompleteResults, { Suggestion } from './AutocompleteResults';
+import dataCatalog from 'catalog/dataCatalog';
+import huePubSub from 'utils/huePubSub';
+import I18n from 'utils/i18n';
+import * as sqlUdfRepository from 'sql/reference/sqlUdfRepository';
+import sqlReferenceRepository from 'sql/reference/sqlReferenceRepository';
+import sleep from 'utils/timing/sleep';
+import * as hueConfig from 'config/hueConfig';
+import { Ace } from 'ext/ace';
+import Executor from '../../../execution/executor';
+import sqlAnalyzerRepository from 'catalog/analyzer/sqlAnalyzerRepository';
+import { AutocompleteParseResult } from 'parse/types';
+import { SetDetails, UdfDetails } from 'sql/reference/types';
+import { HueConfig } from 'config/types';
+
+describe('AutocompleteResults.ts', () => {
+  jest.spyOn(Vue, 'onBeforeUnmount').mockImplementation(() => undefined);
+
+  const sourceMetaSpy = jest
+    .spyOn(CatalogApi, 'fetchSourceMetadata')
+    .mockImplementation(options => {
+      if (options.entry.path.length === 0) {
+        return CancellablePromise.resolve(JSON.parse('{"status": 0, "databases": ["default"]}'));
+      }
+      if (options.entry.path.length === 1) {
+        return CancellablePromise.resolve(
+          JSON.parse(
+            '{"status": 0, "tables_meta": [{"comment": "comment", "type": "Table", "name": "foo"}, {"comment": null, "type": "View", "name": "bar_view"}, {"comment": null, "type": "Table", "name": "bar"}]}'
+          )
+        );
+      }
+      if (options.entry.path.length === 2) {
+        return CancellablePromise.resolve(
+          JSON.parse(
+            '{"status": 0, "support_updates": false, "hdfs_link": "/filebrowser/view=/user/hive/warehouse/customers", "extended_columns": [{"comment": "", "type": "int", "name": "id"}, {"comment": "", "type": "string", "name": "name"}, {"comment": "", "type": "struct<email_format:string,frequency:string,categories:struct<promos:boolean,surveys:boolean>>", "name": "email_preferences"}, {"comment": "", "type": "map<string,struct<street_1:string,street_2:string,city:string,state:string,zip_code:string>>", "name": "addresses"}, {"comment": "", "type": "array<struct<order_id:string,order_date:string,items:array<struct<product_id:int,sku:string,name:string,price:double,qty:int>>>>", "name": "orders"}], "columns": ["id", "name", "email_preferences", "addresses", "orders"], "partition_keys": []}'
+          )
+        );
+      }
+      if (options.entry.path.length === 3) {
+        return CancellablePromise.resolve(
+          JSON.parse(
+            '{"status": 0, "comment": "", "type": "struct", "name": "email_preferences", "fields": [{"type": "string", "name": "email_format"}, {"type": "string", "name": "frequency"}, {"fields": [{"type": "boolean", "name": "promos"}, {"type": "boolean", "name": "surveys"}], "type": "struct", "name": "categories"}]}'
+          )
+        );
+      }
+      if (options.entry.path.length > 3) {
+        return CancellablePromise.resolve(
+          JSON.parse(
+            '{"status": 0, "fields": [{"type": "boolean", "name": "promos"}, {"type": "boolean", "name": "surveys"}], "type": "struct", "name": "categories"}'
+          )
+        );
+      }
+      return CancellablePromise.reject();
+    });
+
+  const createSubject = (): AutocompleteResults => {
+    const mockEditor = () => ({
+      getTextBeforeCursor: () => 'foo',
+      getTextAfterCursor: () => 'bar'
+    });
+
+    const mockExecutor = {
+      connector: () => ({ id: 'hive', dialect: 'hive' }),
+      database: () => 'default',
+      namespace: () => ({ id: 'defaultNamespace' }),
+      compute: () => ({ id: 'defaultCompute' })
+    } as Executor;
+
+    return new AutocompleteResults({
+      temporaryOnly: false,
+      executor: mockExecutor,
+      sqlAnalyzerProvider: sqlAnalyzerRepository,
+      sqlReferenceProvider: sqlReferenceRepository,
+      editor: mockEditor as unknown as Ace.Editor
+    });
+  };
+
+  beforeEach(() => {
+    huePubSub.publish('assist.clear.all.caches');
+    dataCatalog.disableCache();
+  });
+
+  afterEach(() => {
+    dataCatalog.enableCache();
+    jest.resetAllMocks();
+  });
+
+  it('should handle parse results with keywords', async () => {
+    const subject = createSubject();
+    const suggestions: Suggestion[] = [];
+
+    await subject.update(
+      {
+        lowerCase: true,
+        suggestKeywords: [
+          { value: 'BAR', weight: 1 },
+          { value: 'FOO', weight: 2 }
+        ]
+      } as AutocompleteParseResult,
+      suggestions
+    );
+
+    expect(suggestions.length).toBe(2);
+    expect(suggestions[0].meta).toBe(I18n('keyword'));
+    expect(suggestions[0].value).toBe('bar');
+    expect(suggestions[0].weightAdjust).toBe(1);
+    expect(suggestions[1].meta).toBe(I18n('keyword'));
+    expect(suggestions[1].value).toBe('foo');
+    expect(suggestions[1].weightAdjust).toBe(2);
+  });
+
+  it('should handle parse results with identifiers', async () => {
+    const subject = createSubject();
+    const suggestions: Suggestion[] = [];
+
+    await subject.update(
+      {
+        lowerCase: false,
+        suggestIdentifiers: [
+          { name: 'foo', type: 'alias' },
+          { name: 'bar', type: 'table' }
+        ]
+      } as AutocompleteParseResult,
+      suggestions
+    );
+
+    expect(suggestions.length).toBe(2);
+    expect(suggestions[1].meta).toBe('table');
+    expect(suggestions[1].value).toBe('bar');
+    expect(suggestions[0].meta).toBe('alias');
+    expect(suggestions[0].value).toBe('foo');
+  });
+
+  it('should handle parse results with functions', async () => {
+    const spy = jest
+      .spyOn(sqlUdfRepository, 'getUdfsWithReturnTypes')
+      .mockImplementation(async () =>
+        Promise.resolve([
+          {
+            name: 'count',
+            returnTypes: ['BIGINT'],
+            arguments: [[{ type: 'T' }]],
+            signature: 'count(col)',
+            draggable: 'count()',
+            description: 'some desc'
+          }
+        ])
+      );
+    const subject = createSubject();
+    const suggestions: Suggestion[] = [];
+
+    await subject.update(
+      {
+        lowerCase: false,
+        suggestFunctions: {}
+      } as AutocompleteParseResult,
+      suggestions
+    );
+
+    await sleep(0);
+
+    expect(spy).toHaveBeenCalled();
+
+    expect(suggestions.length).toEqual(1);
+    const udfDetails = suggestions[0].details as UdfDetails;
+    expect(udfDetails.arguments).toBeDefined();
+    expect(udfDetails.signature).toBeDefined();
+    expect(udfDetails.description).toBeDefined();
+  });
+
+  it('should handle parse results with udf argument keywords', async () => {
+    const spy = jest
+      .spyOn(sqlUdfRepository, 'getArgumentDetailsForUdf')
+      .mockImplementation(async () => Promise.resolve([{ type: 'T', keywords: ['a', 'b'] }]));
+    const subject = createSubject();
+    const suggestions: Suggestion[] = [];
+
+    await subject.update(
+      {
+        lowerCase: false,
+        udfArgument: {
+          name: 'someudf',
+          position: 1
+        }
+      } as AutocompleteParseResult,
+      suggestions
+    );
+
+    await sleep(0);
+
+    expect(spy).toHaveBeenCalled();
+
+    expect(suggestions.length).toEqual(2);
+    expect(suggestions[0].value).toEqual('a');
+    expect(suggestions[1].value).toEqual('b');
+  });
+
+  it('should handle parse results set options', async () => {
+    const spy = jest.spyOn(sqlReferenceRepository, 'getSetOptions').mockImplementation(
+      async dialect =>
+        new Promise(resolve => {
+          expect(dialect).toEqual(subject.executor.connector().dialect);
+          resolve({
+            OPTION_1: {
+              description: 'Desc 1',
+              type: 'Integer',
+              default: 'Some default'
+            },
+            OPTION_2: {
+              description: 'Desc 2',
+              type: 'Integer',
+              default: 'Some default'
+            }
+          });
+        })
+    );
+    const subject = createSubject();
+    const suggestions: Suggestion[] = [];
+
+    await subject.update(
+      {
+        lowerCase: false,
+        suggestSetOptions: {}
+      } as AutocompleteParseResult,
+      suggestions
+    );
+
+    await sleep(0);
+
+    expect(spy).toHaveBeenCalled();
+
+    expect(suggestions.length).toEqual(2);
+    expect((suggestions[0].details as SetDetails).description).toBeDefined();
+    expect((suggestions[1].details as SetDetails).type).toBeDefined();
+  });
+
+  it('should fetch from source when disable_source_autocomplete is set to false', async () => {
+    jest.spyOn(hueConfig, 'getLastKnownConfig').mockImplementation(
+      () =>
+        ({
+          app_config: {
+            editor: {
+              source_autocomplete_disabled: false
+            }
+          }
+        } as HueConfig)
+    );
+
+    const subject = createSubject();
+    const suggestions: Suggestion[] = [];
+
+    await subject.update(
+      {
+        lowerCase: false,
+        suggestDatabases: {}
+      } as AutocompleteParseResult,
+      suggestions
+    );
+
+    expect(sourceMetaSpy).toHaveBeenCalled();
+  });
+
+  it('should not fetch from source when disable_source_autocomplete is set to true', async () => {
+    jest.spyOn(hueConfig, 'getLastKnownConfig').mockImplementation(
+      () =>
+        ({
+          app_config: {
+            editor: {
+              source_autocomplete_disabled: true
+            }
+          }
+        } as HueConfig)
+    );
+    const subject = createSubject();
+    const suggestions: Suggestion[] = [];
+
+    await subject.update(
+      {
+        lowerCase: false,
+        suggestDatabases: {}
+      } as AutocompleteParseResult,
+      suggestions
+    );
+
+    expect(sourceMetaSpy).not.toHaveBeenCalled();
+  });
+});

+ 39 - 29
desktop/core/src/desktop/js/apps/editor/components/aceEditor/autocomplete/AutocompleteResults.ts

@@ -52,7 +52,7 @@ import { hueWindow } from 'types/types';
 import sqlUtils from 'sql/sqlUtils';
 import { matchesType } from 'sql/reference/typeUtils';
 import { cancelActiveRequest } from 'api/apiUtils';
-import { findBrowserConnector, getRootFilePath } from 'config/hueConfig';
+import { findBrowserConnector, getLastKnownConfig, getRootFilePath } from 'config/hueConfig';
 import {
   findUdf,
   getArgumentDetailsForUdf,
@@ -153,6 +153,7 @@ const locateSubQuery = (subQueries: SubQuery[], subQueryName: string): SubQuery
 class AutocompleteResults {
   executor: Executor;
   editor: Ace.Editor;
+  sourceAutocompleteDisabled: boolean;
   temporaryOnly: boolean;
   activeDatabase: string;
   sqlReferenceProvider: SqlReferenceProvider;
@@ -175,6 +176,8 @@ class AutocompleteResults {
     this.sqlAnalyzer = options.sqlAnalyzerProvider?.getSqlAnalyzer(options.executor.connector());
     this.executor = options.executor;
     this.editor = options.editor;
+    this.sourceAutocompleteDisabled =
+      !!getLastKnownConfig()?.app_config?.editor?.source_autocomplete_disabled;
     this.temporaryOnly = options.temporaryOnly;
     this.activeDatabase = this.executor.database();
   }
@@ -213,31 +216,33 @@ class AutocompleteResults {
       return promise;
     };
 
-    const colRefPromise = this.handleColumnReference();
-    const databasesPromise = this.loadDatabases();
-
     trackPromise(this.handleKeywords());
-    trackPromise(this.handleColRefKeywords(colRefPromise));
     trackPromise(this.handleIdentifiers());
     trackPromise(this.handleColumnAliases());
     trackPromise(this.handleCommonTableExpressions());
     trackPromise(this.handleOptions());
-    trackPromise(this.handleFunctions(colRefPromise));
-    trackPromise(this.handleDatabases(databasesPromise));
-    const tablesPromise = trackPromise(this.handleTables(databasesPromise));
-    const columnsPromise = trackPromise(this.handleColumns(colRefPromise, tablesPromise));
-    trackPromise(this.handleValues(colRefPromise));
-    trackPromise(this.handlePaths());
-
-    if (!this.temporaryOnly) {
-      trackPromise(this.handleJoins());
-      trackPromise(this.handleJoinConditions());
-      trackPromise(this.handleAggregateFunctions());
-      trackPromise(this.handleGroupBys(columnsPromise));
-      trackPromise(this.handleOrderBys(columnsPromise));
-      trackPromise(this.handleFilters());
-      trackPromise(this.handlePopularTables(tablesPromise));
-      trackPromise(this.handlePopularColumns(columnsPromise));
+
+    if (!this.sourceAutocompleteDisabled) {
+      const colRefPromise = this.handleColumnReference();
+      const databasesPromise = this.loadDatabases();
+      trackPromise(this.handleColRefKeywords(colRefPromise));
+      trackPromise(this.handleFunctions(colRefPromise));
+      trackPromise(this.handleDatabases(databasesPromise));
+      const tablesPromise = trackPromise(this.handleTables(databasesPromise));
+      const columnsPromise = trackPromise(this.handleColumns(colRefPromise, tablesPromise));
+      trackPromise(this.handleValues(colRefPromise));
+      trackPromise(this.handlePaths());
+
+      if (!this.temporaryOnly) {
+        trackPromise(this.handleJoins());
+        trackPromise(this.handleJoinConditions());
+        trackPromise(this.handleAggregateFunctions());
+        trackPromise(this.handleGroupBys(columnsPromise));
+        trackPromise(this.handleOrderBys(columnsPromise));
+        trackPromise(this.handleFilters());
+        trackPromise(this.handlePopularTables(tablesPromise));
+        trackPromise(this.handlePopularColumns(columnsPromise));
+      }
     }
 
     return Promise.all(promises);
@@ -433,12 +438,18 @@ class AutocompleteResults {
         });
       } else if (type === 'UDFREF' && columnAlias.udfRef) {
         try {
-          const types = await getReturnTypesForUdf(
-            this.sqlReferenceProvider,
-            this.executor.connector(),
-            columnAlias.udfRef
-          );
-          const resolvedType = types.length === 1 ? types[0] : 'T';
+          let resolvedType = 'T';
+          if (!this.sourceAutocompleteDisabled) {
+            const types = await getReturnTypesForUdf(
+              this.sqlReferenceProvider,
+              this.executor.connector(),
+              columnAlias.udfRef
+            );
+            if (types.length === 1) {
+              resolvedType = types[0];
+            }
+          }
+
           columnAliasSuggestions.push({
             value: columnAlias.name,
             meta: resolvedType,
@@ -1234,8 +1245,7 @@ class AutocompleteResults {
             resolve();
           },
           silenceErrors: true,
-          errorCallback: resolve,
-          timeout: (<hueWindow>window).AUTOCOMPLETE_TIMEOUT
+          errorCallback: resolve
         })
       );
     });

+ 1 - 2
desktop/core/src/desktop/js/apps/editor/snippet.js

@@ -581,8 +581,7 @@ export default class Snippet {
     this.autocompleter = new AceAutocompleteWrapper({
       snippet: this,
       user: this.parentVm.user,
-      optEnabled: false,
-      timeout: this.parentVm.autocompleteTimeout
+      optEnabled: false
     });
 
     this.activeExecutable = ko.observable();

+ 0 - 1
desktop/core/src/desktop/js/apps/notebook/NotebookViewModel.js

@@ -170,7 +170,6 @@ export default class NotebookViewModel {
       );
       return foundInterpreter?.displayName || foundInterpreter?.name || self.editorType();
     });
-    self.autocompleteTimeout = options.autocompleteTimeout;
     self.selectedNotebook = ko.observable();
 
     self.combinedContent = ko.observable();

+ 1 - 4
desktop/core/src/desktop/js/apps/notebook/aceAutocompleteWrapper.js

@@ -22,21 +22,18 @@ class AceAutocompleteWrapper {
    * @param options.snippet
    * @param options.user
    * @param options.optEnabled
-   * @param {Number} options.timeout
    * @constructor
    */
   constructor(options) {
     const self = this;
     self.snippet = options.snippet;
-    self.timeout = options.timeout;
 
     self.topTables = {};
 
     const initializeAutocompleter = function () {
       self.autocompleter = new HdfsAutocompleter({
         user: options.user,
-        snippet: options.snippet,
-        timeout: options.timeout
+        snippet: options.snippet
       });
     };
     if (window.ENABLE_HUE_5) {

+ 1 - 2
desktop/core/src/desktop/js/apps/notebook/snippet.js

@@ -2776,8 +2776,7 @@ class Snippet {
     self.autocompleter = new AceAutocompleteWrapper({
       snippet: self,
       user: vm.user,
-      optEnabled: false,
-      timeout: vm.autocompleteTimeout
+      optEnabled: false
     });
 
     self.init = function () {

+ 1 - 0
desktop/core/src/desktop/js/config/types.ts

@@ -49,6 +49,7 @@ export interface AppConfig<T extends Interpreter> {
 export interface EditorConfig extends AppConfig<EditorInterpreter> {
   default_limit: number | null;
   default_sql_interpreter: string;
+  source_autocomplete_disabled: boolean;
 }
 
 export enum AppType {

+ 0 - 1
desktop/core/src/desktop/js/jest/jest.init.js

@@ -39,7 +39,6 @@ class Autocomplete {}
 
 const globalVars = {
   ko: ko,
-  AUTOCOMPLETE_TIMEOUT: 1,
   CACHEABLE_TTL: 1,
   HAS_LINK_SHARING: true,
   HAS_SQL_ANALYZER: false,

+ 36 - 29
desktop/core/src/desktop/js/sql/autocompleteResults.js

@@ -27,7 +27,7 @@ import sqlUtils from 'sql/sqlUtils';
 import { matchesType } from 'sql/reference/typeUtils';
 import { DIALECT } from 'apps/editor/snippet';
 import { cancelActiveRequest } from 'api/apiUtils';
-import { findBrowserConnector, getRootFilePath } from 'config/hueConfig';
+import { findBrowserConnector, getLastKnownConfig, getRootFilePath } from 'config/hueConfig';
 import equalIgnoreCase from 'utils/string/equalIgnoreCase';
 import {
   findUdf,
@@ -256,6 +256,8 @@ class AutocompleteResults {
     this.snippet = options.snippet;
     this.dialect = () => this.snippet.type();
     this.editor = options.editor;
+    this.sourceAutocompleteDisabled =
+      !!getLastKnownConfig()?.app_config?.editor?.source_autocomplete_disabled;
     this.temporaryOnly =
       options.snippet.autocompleteSettings && options.snippet.autocompleteSettings.temporaryOnly;
 
@@ -450,31 +452,32 @@ class AutocompleteResults {
       return promise;
     };
 
-    const colRefPromise = this.handleColumnReference();
-    const databasesPromise = this.loadDatabases();
-
     trackPromise(this.handleKeywords());
-    trackPromise(this.handleColRefKeywords(colRefPromise));
     trackPromise(this.handleIdentifiers());
     trackPromise(this.handleColumnAliases());
     trackPromise(this.handleCommonTableExpressions());
     trackPromise(this.handleOptions());
-    trackPromise(this.handleFunctions(colRefPromise));
-    trackPromise(this.handleDatabases(databasesPromise));
-    const tablesPromise = trackPromise(this.handleTables(databasesPromise));
-    const columnsPromise = trackPromise(this.handleColumns(colRefPromise, tablesPromise));
-    trackPromise(this.handleValues(colRefPromise));
-    trackPromise(this.handlePaths());
-
-    if (!this.temporaryOnly) {
-      trackPromise(this.handleJoins());
-      trackPromise(this.handleJoinConditions());
-      trackPromise(this.handleAggregateFunctions());
-      trackPromise(this.handleGroupBys(columnsPromise));
-      trackPromise(this.handleOrderBys(columnsPromise));
-      trackPromise(this.handleFilters());
-      trackPromise(this.handlePopularTables(tablesPromise));
-      trackPromise(this.handlePopularColumns(columnsPromise));
+
+    if (!this.sourceAutocompleteDisabled) {
+      const colRefPromise = this.handleColumnReference();
+      const databasesPromise = this.loadDatabases();
+      trackPromise(this.handleColRefKeywords(colRefPromise));
+      trackPromise(this.handleFunctions(colRefPromise));
+      trackPromise(this.handleDatabases(databasesPromise));
+      const tablesPromise = trackPromise(this.handleTables(databasesPromise));
+      const columnsPromise = trackPromise(this.handleColumns(colRefPromise, tablesPromise));
+      trackPromise(this.handleValues(colRefPromise));
+      trackPromise(this.handlePaths());
+      if (!this.temporaryOnly) {
+        trackPromise(this.handleJoins());
+        trackPromise(this.handleJoinConditions());
+        trackPromise(this.handleAggregateFunctions());
+        trackPromise(this.handleGroupBys(columnsPromise));
+        trackPromise(this.handleOrderBys(columnsPromise));
+        trackPromise(this.handleFilters());
+        trackPromise(this.handlePopularTables(tablesPromise));
+        trackPromise(this.handlePopularColumns(columnsPromise));
+      }
     }
 
     await Promise.all(promises);
@@ -672,12 +675,17 @@ class AutocompleteResults {
         });
       } else if (type === 'UDFREF') {
         try {
-          const types = await getReturnTypesForUdf(
-            sqlReferenceRepository,
-            this.snippet.connector(),
-            columnAlias.udfRef
-          );
-          const resolvedType = types.length === 1 ? types[0] : 'T';
+          let resolvedType = 'T';
+          if (!this.sourceAutocompleteDisabled) {
+            const types = await getReturnTypesForUdf(
+              sqlReferenceRepository,
+              this.snippet.connector(),
+              columnAlias.udfRef
+            );
+            if (types.length === 1) {
+              resolvedType = types[0];
+            }
+          }
           columnAliasSuggestions.push({
             value: columnAlias.name,
             meta: resolvedType,
@@ -1483,8 +1491,7 @@ class AutocompleteResults {
             resolve();
           },
           silenceErrors: true,
-          errorCallback: resolve,
-          timeout: AUTOCOMPLETE_TIMEOUT
+          errorCallback: resolve
         })
       );
     });

+ 316 - 303
desktop/core/src/desktop/js/sql/autocompleteResults.test.js

@@ -28,317 +28,77 @@ import LOTS_OF_PARSE_RESULTS from './test/lotsOfParseResults';
 import * as sqlUdfRepository from 'sql/reference/sqlUdfRepository';
 import sqlReferenceRepository from 'sql/reference/sqlReferenceRepository';
 import sleep from 'utils/timing/sleep';
+import * as hueConfig from 'config/hueConfig';
 
 describe('AutocompleteResults.js', () => {
-  const failResponse = {
-    status: 500
-  };
-
-  jest.spyOn(CatalogApi, 'fetchSourceMetadata').mockImplementation(options => {
-    if (Math.random() < 0.5) {
-      return CancellablePromise.reject(failResponse);
-    }
-    if (options.entry.path.length === 0) {
-      return CancellablePromise.resolve(JSON.parse('{"status": 0, "databases": ["default"]}'));
-    }
-    if (options.entry.path.length === 1) {
-      return CancellablePromise.resolve(
-        JSON.parse(
-          '{"status": 0, "tables_meta": [{"comment": "comment", "type": "Table", "name": "foo"}, {"comment": null, "type": "View", "name": "bar_view"}, {"comment": null, "type": "Table", "name": "bar"}]}'
-        )
-      );
-    }
-    if (options.entry.path.length === 2) {
-      return CancellablePromise.resolve(
-        JSON.parse(
-          '{"status": 0, "support_updates": false, "hdfs_link": "/filebrowser/view=/user/hive/warehouse/customers", "extended_columns": [{"comment": "", "type": "int", "name": "id"}, {"comment": "", "type": "string", "name": "name"}, {"comment": "", "type": "struct<email_format:string,frequency:string,categories:struct<promos:boolean,surveys:boolean>>", "name": "email_preferences"}, {"comment": "", "type": "map<string,struct<street_1:string,street_2:string,city:string,state:string,zip_code:string>>", "name": "addresses"}, {"comment": "", "type": "array<struct<order_id:string,order_date:string,items:array<struct<product_id:int,sku:string,name:string,price:double,qty:int>>>>", "name": "orders"}], "columns": ["id", "name", "email_preferences", "addresses", "orders"], "partition_keys": []}'
-        )
-      );
-    }
-    if (options.entry.path.length === 3) {
-      return CancellablePromise.resolve(
-        JSON.parse(
-          '{"status": 0, "comment": "", "type": "struct", "name": "email_preferences", "fields": [{"type": "string", "name": "email_format"}, {"type": "string", "name": "frequency"}, {"fields": [{"type": "boolean", "name": "promos"}, {"type": "boolean", "name": "surveys"}], "type": "struct", "name": "categories"}]}'
-        )
-      );
-    }
-    if (options.entry.path.length > 3) {
-      return CancellablePromise.resolve(
-        JSON.parse(
-          '{"status": 0, "fields": [{"type": "boolean", "name": "promos"}, {"type": "boolean", "name": "surveys"}], "type": "struct", "name": "categories"}'
-        )
-      );
-    }
-    return CancellablePromise.reject();
-  });
-
-  jest.spyOn(ApiHelper, 'fetchHdfsPath').mockImplementation(options => {
-    const deferred = $.Deferred();
-
-    deferred.done(options.successCallback);
-
-    deferred.resolve({
-      superuser: 'hdfs',
-      current_request_path: '/filebrowser/view=///var',
-      current_dir_path: '///var',
-      show_download_button: true,
-      cwd_set: true,
-      breadcrumbs: [
-        {
-          url: '/',
-          label: '/'
-        },
-        {
-          url: '/var',
-          label: 'var'
-        }
-      ],
-      apps: [
-        'help',
-        'sqoop',
-        'pig',
-        'hbase',
-        'rdbms',
-        'indexer',
-        'metastore',
-        'beeswax',
-        'jobsub',
-        'metadata',
-        'zookeeper',
-        'search',
-        'useradmin',
-        'notebook',
-        'proxy',
-        'oozie',
-        'spark',
-        'filebrowser',
-        'about',
-        'jobbrowser',
-        'dashboard',
-        'security',
-        'impala'
-      ],
-      show_upload_button: true,
-      files: [
-        {
-          humansize: '0\u00a0bytes',
-          url: '/filebrowser/view=/',
-          stats: {
-            size: 0,
-            group: 'supergroup',
-            blockSize: 0,
-            replication: 0,
-            user: 'hdfs',
-            mtime: 1476970119,
-            path: '///var/..',
-            atime: 0,
-            mode: 16877
-          },
-          name: '..',
-          mtime: 'October 20, 2016 06:28 AM',
-          rwx: 'drwxr-xr-x',
-          path: '/',
-          is_sentry_managed: false,
-          type: 'dir',
-          mode: '40755'
-        },
-        {
-          humansize: '0\u00a0bytes',
-          url: '/filebrowser/view=/var',
-          stats: {
-            size: 0,
-            group: 'supergroup',
-            blockSize: 0,
-            replication: 0,
-            user: 'hdfs',
-            mtime: 1470887321,
-            path: '///var',
-            atime: 0,
-            mode: 16877
-          },
-          name: '.',
-          mtime: 'August 10, 2016 08:48 PM',
-          rwx: 'drwxr-xr-x',
-          path: '/var',
-          is_sentry_managed: false,
-          type: 'dir',
-          mode: '40755'
-        },
-        {
-          humansize: '0\u00a0bytes',
-          url: '/filebrowser/view=/var/lib',
-          stats: {
-            size: 0,
-            group: 'supergroup',
-            blockSize: 0,
-            replication: 0,
-            user: 'hdfs',
-            mtime: 1470887321,
-            path: '/var/lib',
-            atime: 0,
-            mode: 16877
-          },
-          name: 'lib',
-          mtime: 'August 10, 2016 08:48 PM',
-          rwx: 'drwxr-xr-x',
-          path: '/var/lib',
-          is_sentry_managed: false,
-          type: 'dir',
-          mode: '40755'
-        },
-        {
-          humansize: '0\u00a0bytes',
-          url: '/filebrowser/view=/var/log',
-          stats: {
-            size: 0,
-            group: 'mapred',
-            blockSize: 0,
-            replication: 0,
-            user: 'yarn',
-            mtime: 1470887196,
-            path: '/var/log',
-            atime: 0,
-            mode: 17405
-          },
-          name: 'log',
-          mtime: 'August 10, 2016 08:46 PM',
-          rwx: 'drwxrwxr-xt',
-          path: '/var/log',
-          is_sentry_managed: false,
-          type: 'dir',
-          mode: '41775'
-        }
-      ],
-      users: [],
-      is_embeddable: false,
-      supergroup: 'supergroup',
-      descending: 'false',
-      groups: [],
-      is_trash_enabled: true,
-      pagesize: 50,
-      file_filter: 'any',
-      is_fs_superuser: false,
-      is_sentry_managed: false,
-      home_directory: '/user/admin',
-      path: '///var',
-      page: {
-        num_pages: 1,
-        total_count: 2,
-        next_page_number: 1,
-        end_index: 2,
-        number: 1,
-        previous_page_number: 1,
-        start_index: 1
+  const sourceMetaSpy = jest
+    .spyOn(CatalogApi, 'fetchSourceMetadata')
+    .mockImplementation(options => {
+      if (options.entry.path.length === 0) {
+        return CancellablePromise.resolve(JSON.parse('{"status": 0, "databases": ["default"]}'));
       }
-    });
-
-    return deferred.promise();
-  });
-
-  jest.spyOn(apiUtils, 'fetchUdfs').mockImplementation(() => Promise.resolve([]));
-
-  const subject = new AutocompleteResults({
-    snippet: {
-      autocompleteSettings: {
-        temporaryOnly: false
-      },
-      type: function () {
-        return 'hive';
-      },
-      connector: function () {
-        return { id: 'hive', dialect: 'hive' };
-      },
-      database: function () {
-        return 'default';
-      },
-      namespace: function () {
-        return { id: 'defaultNamespace' };
-      },
-      compute: function () {
-        return { id: 'defaultCompute' };
-      },
-      whenContextSet: function () {
-        return $.Deferred().resolve();
+      if (options.entry.path.length === 1) {
+        return CancellablePromise.resolve(
+          JSON.parse(
+            '{"status": 0, "tables_meta": [{"comment": "comment", "type": "Table", "name": "foo"}, {"comment": null, "type": "View", "name": "bar_view"}, {"comment": null, "type": "Table", "name": "bar"}]}'
+          )
+        );
       }
-    },
-    editor: function () {
-      return {
-        getTextBeforeCursor: function () {
-          return 'foo';
-        },
-        getTextAfterCursor: function () {
-          return 'bar';
-        }
-      };
-    }
-  });
-
-  describe('Test a whole lot of different parse results', () => {
-    const LOADING_OBSERVABLES = [
-      'loadingKeywords',
-      'loadingFunctions',
-      'loadingDatabases',
-      'loadingTables',
-      'loadingColumns',
-      'loadingValues',
-      'loadingPaths',
-      'loadingJoins',
-      'loadingJoinConditions',
-      'loadingAggregateFunctions',
-      'loadingGroupBys',
-      'loadingOrderBys',
-      'loadingFilters',
-      'loadingPopularTables',
-      'loadingPopularColumns'
-    ];
-
-    beforeEach(() => {
-      dataCatalog.disableCache();
-      window.AUTOCOMPLETE_TIMEOUT = 1;
-      global.AUTOCOMPLETE_TIMEOUT = 1;
-
-      huePubSub.publish('assist.clear.all.caches');
+      if (options.entry.path.length === 2) {
+        return CancellablePromise.resolve(
+          JSON.parse(
+            '{"status": 0, "support_updates": false, "hdfs_link": "/filebrowser/view=/user/hive/warehouse/customers", "extended_columns": [{"comment": "", "type": "int", "name": "id"}, {"comment": "", "type": "string", "name": "name"}, {"comment": "", "type": "struct<email_format:string,frequency:string,categories:struct<promos:boolean,surveys:boolean>>", "name": "email_preferences"}, {"comment": "", "type": "map<string,struct<street_1:string,street_2:string,city:string,state:string,zip_code:string>>", "name": "addresses"}, {"comment": "", "type": "array<struct<order_id:string,order_date:string,items:array<struct<product_id:int,sku:string,name:string,price:double,qty:int>>>>", "name": "orders"}], "columns": ["id", "name", "email_preferences", "addresses", "orders"], "partition_keys": []}'
+          )
+        );
+      }
+      if (options.entry.path.length === 3) {
+        return CancellablePromise.resolve(
+          JSON.parse(
+            '{"status": 0, "comment": "", "type": "struct", "name": "email_preferences", "fields": [{"type": "string", "name": "email_format"}, {"type": "string", "name": "frequency"}, {"fields": [{"type": "boolean", "name": "promos"}, {"type": "boolean", "name": "surveys"}], "type": "struct", "name": "categories"}]}'
+          )
+        );
+      }
+      if (options.entry.path.length > 3) {
+        return CancellablePromise.resolve(
+          JSON.parse(
+            '{"status": 0, "fields": [{"type": "boolean", "name": "promos"}, {"type": "boolean", "name": "surveys"}], "type": "struct", "name": "categories"}'
+          )
+        );
+      }
+      return CancellablePromise.reject();
     });
 
-    afterEach(() => {
-      AUTOCOMPLETE_TIMEOUT = 0;
-      dataCatalog.enableCache();
+  const createSubject = () =>
+    new AutocompleteResults({
+      snippet: {
+        autocompleteSettings: {
+          temporaryOnly: false
+        },
+        type: () => 'hive',
+        connector: () => ({ id: 'hive', dialect: 'hive' }),
+        database: () => 'default',
+        namespace: () => ({ id: 'defaultNamespace' }),
+        compute: () => ({ id: 'defaultCompute' }),
+        whenContextSet: () => Promise.resolve()
+      },
+      editor: () => ({
+        getTextBeforeCursor: () => 'foo',
+        getTextAfterCursor: () => 'bar'
+      })
     });
 
-    for (const parseResult of LOTS_OF_PARSE_RESULTS) {
-      it('should handle parse result no. ' + parseResult.index, async () => {
-        if (parseResult.suggestKeywords) {
-          const cleanedKeywords = [];
-          parseResult.suggestKeywords.forEach(keyword => {
-            if (!keyword.value) {
-              cleanedKeywords.push({ value: keyword });
-            } else {
-              cleanedKeywords.push(keyword);
-            }
-          });
-          parseResult.suggestKeywords = cleanedKeywords;
-        }
-        try {
-          await subject.update(parseResult);
-        } catch (e) {
-          fail('Got exception');
-          console.error(e);
-        }
-        if (subject.loading()) {
-          LOADING_OBSERVABLES.forEach(observable => {
-            if (subject[observable]()) {
-              fail('Still loading (' + observable + '() == true), missing ajax spec?');
-            }
-          });
-        }
+  beforeEach(() => {
+    huePubSub.publish('assist.clear.all.caches');
+    dataCatalog.disableCache();
+  });
 
-        expect(subject.loading()).toBeFalsy();
-      });
-    }
+  afterEach(() => {
+    dataCatalog.enableCache();
+    jest.resetAllMocks();
   });
 
   it('should handle parse results with keywords', async () => {
-    subject.entries([]);
+    const subject = createSubject();
 
     expect(subject.filtered().length).toBe(0);
 
@@ -359,7 +119,7 @@ describe('AutocompleteResults.js', () => {
   });
 
   it('should handle parse results with identifiers', async () => {
-    subject.entries([]);
+    const subject = createSubject();
 
     expect(subject.filtered().length).toBe(0);
     await subject.update({
@@ -379,7 +139,7 @@ describe('AutocompleteResults.js', () => {
   });
 
   it('should handle parse results with functions', async () => {
-    subject.entries([]);
+    const subject = createSubject();
 
     const spy = jest
       .spyOn(sqlUdfRepository, 'getUdfsWithReturnTypes')
@@ -414,7 +174,7 @@ describe('AutocompleteResults.js', () => {
   });
 
   it('should handle parse results with udf argument keywords', async () => {
-    subject.entries([]);
+    const subject = createSubject();
 
     const spy = jest
       .spyOn(sqlUdfRepository, 'getArgumentDetailsForUdf')
@@ -440,7 +200,7 @@ describe('AutocompleteResults.js', () => {
   });
 
   it('should handle parse results set options', async () => {
-    subject.entries([]);
+    const subject = createSubject();
 
     const spy = jest.spyOn(sqlReferenceRepository, 'getSetOptions').mockImplementation(
       async dialect =>
@@ -476,4 +236,257 @@ describe('AutocompleteResults.js', () => {
     expect(subject.filtered()[0].details.description).toBeDefined();
     expect(subject.filtered()[1].details.type).toBeDefined();
   });
+
+  it('should fetch from source when disable_source_autocomplete is set to false', async () => {
+    jest.spyOn(hueConfig, 'getLastKnownConfig').mockImplementation(() => ({
+      app_config: {
+        editor: {
+          source_autocomplete_disabled: false
+        }
+      }
+    }));
+
+    const subject = createSubject();
+
+    await subject.update({
+      lowerCase: false,
+      suggestDatabases: {}
+    });
+
+    expect(sourceMetaSpy).toHaveBeenCalled();
+  });
+
+  it('should not fetch from source when disable_source_autocomplete is set to true', async () => {
+    jest.spyOn(hueConfig, 'getLastKnownConfig').mockImplementation(() => ({
+      app_config: {
+        editor: {
+          source_autocomplete_disabled: true
+        }
+      }
+    }));
+    const subject = createSubject();
+
+    await subject.update({
+      lowerCase: false,
+      suggestDatabases: {}
+    });
+
+    expect(sourceMetaSpy).not.toHaveBeenCalled();
+  });
+
+  describe('Test a whole lot of different parse results', () => {
+    const LOADING_OBSERVABLES = [
+      'loadingKeywords',
+      'loadingFunctions',
+      'loadingDatabases',
+      'loadingTables',
+      'loadingColumns',
+      'loadingValues',
+      'loadingPaths',
+      'loadingJoins',
+      'loadingJoinConditions',
+      'loadingAggregateFunctions',
+      'loadingGroupBys',
+      'loadingOrderBys',
+      'loadingFilters',
+      'loadingPopularTables',
+      'loadingPopularColumns'
+    ];
+
+    for (const parseResult of LOTS_OF_PARSE_RESULTS) {
+      it('should handle parse result no. ' + parseResult.index, async () => {
+        jest.spyOn(apiUtils, 'fetchUdfs').mockImplementation(() => Promise.resolve([]));
+        jest.spyOn(ApiHelper, 'fetchHdfsPath').mockImplementation(options => {
+          const deferred = $.Deferred();
+
+          deferred.done(options.successCallback);
+
+          deferred.resolve({
+            superuser: 'hdfs',
+            current_request_path: '/filebrowser/view=///var',
+            current_dir_path: '///var',
+            show_download_button: true,
+            cwd_set: true,
+            breadcrumbs: [
+              {
+                url: '/',
+                label: '/'
+              },
+              {
+                url: '/var',
+                label: 'var'
+              }
+            ],
+            apps: [
+              'help',
+              'sqoop',
+              'pig',
+              'hbase',
+              'rdbms',
+              'indexer',
+              'metastore',
+              'beeswax',
+              'jobsub',
+              'metadata',
+              'zookeeper',
+              'search',
+              'useradmin',
+              'notebook',
+              'proxy',
+              'oozie',
+              'spark',
+              'filebrowser',
+              'about',
+              'jobbrowser',
+              'dashboard',
+              'security',
+              'impala'
+            ],
+            show_upload_button: true,
+            files: [
+              {
+                humansize: '0\u00a0bytes',
+                url: '/filebrowser/view=/',
+                stats: {
+                  size: 0,
+                  group: 'supergroup',
+                  blockSize: 0,
+                  replication: 0,
+                  user: 'hdfs',
+                  mtime: 1476970119,
+                  path: '///var/..',
+                  atime: 0,
+                  mode: 16877
+                },
+                name: '..',
+                mtime: 'October 20, 2016 06:28 AM',
+                rwx: 'drwxr-xr-x',
+                path: '/',
+                is_sentry_managed: false,
+                type: 'dir',
+                mode: '40755'
+              },
+              {
+                humansize: '0\u00a0bytes',
+                url: '/filebrowser/view=/var',
+                stats: {
+                  size: 0,
+                  group: 'supergroup',
+                  blockSize: 0,
+                  replication: 0,
+                  user: 'hdfs',
+                  mtime: 1470887321,
+                  path: '///var',
+                  atime: 0,
+                  mode: 16877
+                },
+                name: '.',
+                mtime: 'August 10, 2016 08:48 PM',
+                rwx: 'drwxr-xr-x',
+                path: '/var',
+                is_sentry_managed: false,
+                type: 'dir',
+                mode: '40755'
+              },
+              {
+                humansize: '0\u00a0bytes',
+                url: '/filebrowser/view=/var/lib',
+                stats: {
+                  size: 0,
+                  group: 'supergroup',
+                  blockSize: 0,
+                  replication: 0,
+                  user: 'hdfs',
+                  mtime: 1470887321,
+                  path: '/var/lib',
+                  atime: 0,
+                  mode: 16877
+                },
+                name: 'lib',
+                mtime: 'August 10, 2016 08:48 PM',
+                rwx: 'drwxr-xr-x',
+                path: '/var/lib',
+                is_sentry_managed: false,
+                type: 'dir',
+                mode: '40755'
+              },
+              {
+                humansize: '0\u00a0bytes',
+                url: '/filebrowser/view=/var/log',
+                stats: {
+                  size: 0,
+                  group: 'mapred',
+                  blockSize: 0,
+                  replication: 0,
+                  user: 'yarn',
+                  mtime: 1470887196,
+                  path: '/var/log',
+                  atime: 0,
+                  mode: 17405
+                },
+                name: 'log',
+                mtime: 'August 10, 2016 08:46 PM',
+                rwx: 'drwxrwxr-xt',
+                path: '/var/log',
+                is_sentry_managed: false,
+                type: 'dir',
+                mode: '41775'
+              }
+            ],
+            users: [],
+            is_embeddable: false,
+            supergroup: 'supergroup',
+            descending: 'false',
+            groups: [],
+            is_trash_enabled: true,
+            pagesize: 50,
+            file_filter: 'any',
+            is_fs_superuser: false,
+            is_sentry_managed: false,
+            home_directory: '/user/admin',
+            path: '///var',
+            page: {
+              num_pages: 1,
+              total_count: 2,
+              next_page_number: 1,
+              end_index: 2,
+              number: 1,
+              previous_page_number: 1,
+              start_index: 1
+            }
+          });
+
+          return deferred.promise();
+        });
+
+        const subject = createSubject();
+        if (parseResult.suggestKeywords) {
+          const cleanedKeywords = [];
+          parseResult.suggestKeywords.forEach(keyword => {
+            if (!keyword.value) {
+              cleanedKeywords.push({ value: keyword });
+            } else {
+              cleanedKeywords.push(keyword);
+            }
+          });
+          parseResult.suggestKeywords = cleanedKeywords;
+        }
+        try {
+          await subject.update(parseResult);
+        } catch (e) {
+          fail('Got exception');
+          console.error(e);
+        }
+        if (subject.loading()) {
+          LOADING_OBSERVABLES.forEach(observable => {
+            if (subject[observable]()) {
+              fail('Still loading (' + observable + '() == true), missing ajax spec?');
+            }
+          });
+        }
+
+        expect(subject.loading()).toBeFalsy();
+      });
+    }
+  });
 });

+ 0 - 2
desktop/core/src/desktop/js/sql/sqlAutocompleter.test.ignore.js

@@ -27,7 +27,6 @@ xdescribe('sqlAutocomplete.js', () => {
 
   beforeEach(() => {
     dataCatalog.disableCache();
-    window.AUTOCOMPLETE_TIMEOUT = 1;
     jasmine.Ajax.install();
 
     jasmine.Ajax.stubRequest(/.*\/notebook\/api\/autocomplete\/$/).mockReturnValue({
@@ -65,7 +64,6 @@ xdescribe('sqlAutocomplete.js', () => {
       }
       fail('Still loading, missing ajax spec?');
     }
-    AUTOCOMPLETE_TIMEOUT = 0;
     dataCatalog.enableCache();
     jasmine.Ajax.uninstall();
   });

+ 0 - 1
desktop/core/src/desktop/js/types/types.ts

@@ -44,7 +44,6 @@ declare global {
 }
 
 export interface hueWindow {
-  AUTOCOMPLETE_TIMEOUT?: number;
   CACHEABLE_TTL?: { default?: number; sqlAnalyzer?: number };
   CLOSE_SESSIONS?: { [dialect: string]: boolean };
   CUSTOM_DASHBOARD_URL?: string;

+ 0 - 3
desktop/core/src/desktop/js/utils/hdfsAutocompleter.js

@@ -22,7 +22,6 @@ class HdfsAutocompleter {
   /**
    * @param {object} options
    * @param {string} options.user
-   * @param {Number} options.timeout
    * @param {Snippet} options.snippet
    *
    * @constructor
@@ -31,7 +30,6 @@ class HdfsAutocompleter {
     const self = this;
     self.user = options.user;
     self.snippet = options.snippet;
-    self.timeout = options.timeout;
   }
 
   hasExpired(timestamp) {
@@ -88,7 +86,6 @@ class HdfsAutocompleter {
         successCallback: successCallback,
         silenceErrors: true,
         errorCallback: onFailure,
-        timeout: self.timeout,
         editor: editor
       });
     } else {

+ 5 - 4
desktop/core/src/desktop/models.py

@@ -50,9 +50,9 @@ from useradmin.organization import _fitered_queryset
 
 from desktop import appmanager
 from desktop.auth.backend import is_admin
-from desktop.conf import get_clusters, IS_MULTICLUSTER_ONLY, ENABLE_ORGANIZATIONS, ENABLE_PROMETHEUS, \
-    has_connectors, TASK_SERVER, APP_BLACKLIST, COLLECT_USAGE, ENABLE_SHARING, ENABLE_CONNECTORS, ENABLE_UNIFIED_ANALYTICS, RAZ, \
-    HUE_IMAGE_VERSION, HUE_HOST_NAME
+from desktop.conf import APP_BLACKLIST, COLLECT_USAGE, DISABLE_SOURCE_AUTOCOMPLETE, ENABLE_CONNECTORS, \
+    ENABLE_ORGANIZATIONS, ENABLE_PROMETHEUS, ENABLE_SHARING, ENABLE_UNIFIED_ANALYTICS, get_clusters, has_connectors, \
+    HUE_HOST_NAME, HUE_IMAGE_VERSION, IS_MULTICLUSTER_ONLY, RAZ, TASK_SERVER
 from desktop.lib import fsmanager
 from desktop.lib.connectors.api import _get_installed_connectors
 from desktop.lib.connectors.models import Connector
@@ -1930,7 +1930,8 @@ class ClusterConfig(object):
         'default_limit': None if DEFAULT_LIMIT.get() == 0 else DEFAULT_LIMIT.get(),
         'interpreter_names': [interpreter['type'] for interpreter in interpreters],
         'page': interpreters[0]['page'],
-        'default_sql_interpreter': next((interpreter['type'] for interpreter in interpreters if interpreter.get('is_sql')), 'hive')
+        'default_sql_interpreter': next((interpreter['type'] for interpreter in interpreters if interpreter.get('is_sql')), 'hive'),
+        'source_autocomplete_disabled': DISABLE_SOURCE_AUTOCOMPLETE.get()
       }
     else:
       return None

+ 0 - 2
desktop/core/src/desktop/templates/common_header_m.mako

@@ -88,8 +88,6 @@ if USE_NEW_EDITOR.get():
       sqlAnalyzer: ${ OPTIMIZER.CACHEABLE_TTL.get() }
     };
 
-    var AUTOCOMPLETE_TIMEOUT = ${ conf.EDITOR_AUTOCOMPLETE_TIMEOUT.get() };
-
     window.LEAFLET_DEFAULTS = {
       layer: '${ leaflet['layer'] |n,unicode }',
       attribution: '${ leaflet['attribution'] |n,unicode }'

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

@@ -53,7 +53,6 @@
   <%
     current_app, other_apps, apps = _get_apps(user)
   %>
-  window.AUTOCOMPLETE_TIMEOUT = ${ conf.EDITOR_AUTOCOMPLETE_TIMEOUT.get() };
   window.BANNER_TOP_HTML = '${ conf.CUSTOM.BANNER_TOP_HTML.get() }';
   window.DISABLE_LOCAL_STORAGE = '${ conf.DISABLE_LOCAL_STORAGE.get() }' === 'True';
 

+ 0 - 1
desktop/libs/notebook/src/notebook/templates/editor2.mako

@@ -1082,7 +1082,6 @@ There is no bridge to KO for components using this integration. Example using in
     userId: ${ user.id },
     suffix: 'editor',
     assistAvailable: true,
-    autocompleteTimeout: AUTOCOMPLETE_TIMEOUT,
     snippetViewSettings: {
       default: {
         placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',

+ 0 - 1
desktop/libs/notebook/src/notebook/templates/editor_components.mako

@@ -2070,7 +2070,6 @@ ${ sqlSyntaxDropdown.sqlSyntaxDropdown() }
     userId: ${ user.id },
     suffix: '${ suffix }',
     assistAvailable: true,
-    autocompleteTimeout: AUTOCOMPLETE_TIMEOUT,
     snippetViewSettings: {
       default: {
         placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',

+ 0 - 1
desktop/libs/notebook/src/notebook/templates/editor_m.mako

@@ -176,7 +176,6 @@ ${ commonheader_m(editor_type, editor_type, user, request, "68px") | n,unicode }
     user: '${ user.username }',
     userId: ${ user.id },
     assistAvailable: true,
-    autocompleteTimeout: AUTOCOMPLETE_TIMEOUT,
     snippetViewSettings: {
       default: {
         placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',