Browse Source

[frontend] Split hueUtils over multiple modules

This removes the jquery dependency for most uses of the util modules.
Johan Åhlén 4 years ago
parent
commit
edcaf1fc89
100 changed files with 1230 additions and 942 deletions
  1. 3 2
      desktop/core/src/desktop/js/api/apiUtils.js
  2. 4 4
      desktop/core/src/desktop/js/api/utils.ts
  3. 17 15
      desktop/core/src/desktop/js/apps/editor/EditorViewModel.js
  4. 5 4
      desktop/core/src/desktop/js/apps/editor/app.js
  5. 1 1
      desktop/core/src/desktop/js/apps/editor/components/ExecuteButton.test.ts
  6. 1 1
      desktop/core/src/desktop/js/apps/editor/components/aceEditor/AceEditor.vue
  7. 1 1
      desktop/core/src/desktop/js/apps/editor/components/aceEditor/AceLocationHandler.ts
  8. 2 1
      desktop/core/src/desktop/js/apps/editor/components/aceEditor/acePredict.ts
  9. 10 14
      desktop/core/src/desktop/js/apps/editor/components/aceEditor/autocomplete/AutocompleteResults.ts
  10. 1 1
      desktop/core/src/desktop/js/apps/editor/components/result/ResultTable.vue
  11. 1 1
      desktop/core/src/desktop/js/apps/editor/components/resultChart/chartTransformers.js
  12. 2 1
      desktop/core/src/desktop/js/apps/editor/components/resultChart/ko.resultChart.js
  13. 4 4
      desktop/core/src/desktop/js/apps/editor/components/resultGrid/ko.resultDownloadActions.js
  14. 1 1
      desktop/core/src/desktop/js/apps/editor/components/resultGrid/ko.resultDownloadModal.js
  15. 2 1
      desktop/core/src/desktop/js/apps/editor/components/resultGrid/ko.resultGrid.js
  16. 3 1
      desktop/core/src/desktop/js/apps/editor/components/resultGrid/ko.simpleResultGrid.js
  17. 1 1
      desktop/core/src/desktop/js/apps/editor/components/sqlScratchpad/SqlScratchpad.vue
  18. 1 1
      desktop/core/src/desktop/js/apps/editor/components/variableSubstitution/VariableSubstitution.vue
  19. 2 6
      desktop/core/src/desktop/js/apps/editor/execution/executable.ts
  20. 1 1
      desktop/core/src/desktop/js/apps/editor/execution/executionResult.ts
  21. 3 3
      desktop/core/src/desktop/js/apps/editor/notebook.js
  22. 2 1
      desktop/core/src/desktop/js/apps/editor/snippet.js
  23. 18 15
      desktop/core/src/desktop/js/apps/notebook/NotebookViewModel.js
  24. 16 17
      desktop/core/src/desktop/js/apps/notebook/app.js
  25. 7 7
      desktop/core/src/desktop/js/apps/notebook/chartTransformers.js
  26. 5 6
      desktop/core/src/desktop/js/apps/notebook/notebook.js
  27. 2 2
      desktop/core/src/desktop/js/apps/notebook/result.js
  28. 2 2
      desktop/core/src/desktop/js/apps/notebook/snippet.js
  29. 6 4
      desktop/core/src/desktop/js/apps/tableBrowser/app.js
  30. 2 2
      desktop/core/src/desktop/js/apps/tableBrowser/metastoreTable.js
  31. 6 5
      desktop/core/src/desktop/js/apps/tableBrowser/metastoreViewModel.js
  32. 2 1
      desktop/core/src/desktop/js/catalog/api.ts
  33. 1 1
      desktop/core/src/desktop/js/catalog/contextCatalog.ts
  34. 1 1
      desktop/core/src/desktop/js/components/HueLink.vue
  35. 5 4
      desktop/core/src/desktop/js/components/SqlText.vue
  36. 1 1
      desktop/core/src/desktop/js/components/directives/clickOutsideDirective.ts
  37. 1 1
      desktop/core/src/desktop/js/components/dropdown/DropdownDrawer.vue
  38. 1 1
      desktop/core/src/desktop/js/components/er-diagram/index.vue
  39. 1 1
      desktop/core/src/desktop/js/components/sidebar/HueSidebar.vue
  40. 1 1
      desktop/core/src/desktop/js/components/sidebar/SidebarBody.vue
  41. 1 1
      desktop/core/src/desktop/js/components/sidebar/SidebarDrawer.vue
  42. 5 7
      desktop/core/src/desktop/js/doc/hueDocument.js
  43. 1 1
      desktop/core/src/desktop/js/jquery/plugins/jquery.hdfs.autocomplete.js
  44. 2 2
      desktop/core/src/desktop/js/jquery/plugins/jquery.hdfstree.js
  45. 5 5
      desktop/core/src/desktop/js/jquery/plugins/jquery.huedatatable.js
  46. 3 2
      desktop/core/src/desktop/js/jquery/plugins/jquery.notify.js
  47. 2 2
      desktop/core/src/desktop/js/jquery/plugins/jquery.tableextender2.js
  48. 2 2
      desktop/core/src/desktop/js/ko/bindings/charts/ko.pieChart.js
  49. 2 2
      desktop/core/src/desktop/js/ko/bindings/ko.codemirror.js
  50. 3 3
      desktop/core/src/desktop/js/ko/bindings/ko.dockable.js
  51. 2 2
      desktop/core/src/desktop/js/ko/bindings/ko.fileChooser.js
  52. 2 2
      desktop/core/src/desktop/js/ko/bindings/ko.fresherEditor.js
  53. 3 3
      desktop/core/src/desktop/js/ko/bindings/ko.html.js
  54. 2 2
      desktop/core/src/desktop/js/ko/bindings/ko.logScroller.js
  55. 2 2
      desktop/core/src/desktop/js/ko/bindings/ko.toggleOverflow.js
  56. 4 3
      desktop/core/src/desktop/js/ko/bindings/ko.toggleOverflow.test.js
  57. 3 2
      desktop/core/src/desktop/js/ko/bindings/ko.tooltip.js
  58. 1 1
      desktop/core/src/desktop/js/ko/components/assist/ko.assistFunctionsPanel.test.js
  59. 1 1
      desktop/core/src/desktop/js/ko/components/assist/ko.assistLangRefPanel.test.js
  60. 1 1
      desktop/core/src/desktop/js/ko/components/ko.catalogEntriesList.js
  61. 1 1
      desktop/core/src/desktop/js/ko/components/ko.editorDroppableMenu.js
  62. 2 2
      desktop/core/src/desktop/js/ko/components/ko.fieldSamples.js
  63. 2 2
      desktop/core/src/desktop/js/ko/components/ko.sentryPrivileges.js
  64. 1 1
      desktop/core/src/desktop/js/ko/components/simpleAceEditor/ko.simpleAceEditor.js
  65. 5 12
      desktop/core/src/desktop/js/nvd3/nv.d3.lineWithBrushChart.js
  66. 5 12
      desktop/core/src/desktop/js/nvd3/nv.d3.multiBarWithBrushChart.js
  67. 13 10
      desktop/core/src/desktop/js/onePageViewModel.js
  68. 1 1
      desktop/core/src/desktop/js/sidePanelViewModel.js
  69. 11 13
      desktop/core/src/desktop/js/sql/autocompleteResults.js
  70. 2 2
      desktop/core/src/desktop/js/sql/autocompleteResults.test.js
  71. 1 3
      desktop/core/src/desktop/js/sql/sqlWorkerHandler.js
  72. 1 0
      desktop/core/src/desktop/js/types/types.ts
  73. 41 0
      desktop/core/src/desktop/js/utils/html/bootstrapRatios.ts
  74. 69 0
      desktop/core/src/desktop/js/utils/html/deXSS.test.ts
  75. 22 0
      desktop/core/src/desktop/js/utils/html/deXSS.ts
  76. 21 0
      desktop/core/src/desktop/js/utils/html/escapeOutput.ts
  77. 193 0
      desktop/core/src/desktop/js/utils/html/getFileBrowseButton.ts
  78. 30 0
      desktop/core/src/desktop/js/utils/html/getStyleFromCSSClass.ts
  79. 44 0
      desktop/core/src/desktop/js/utils/html/highlight.ts
  80. 23 0
      desktop/core/src/desktop/js/utils/html/html2text.ts
  81. 27 0
      desktop/core/src/desktop/js/utils/html/htmlEncode.ts
  82. 25 0
      desktop/core/src/desktop/js/utils/html/isOverflowing.ts
  83. 39 0
      desktop/core/src/desktop/js/utils/html/onHueLinkClick.ts
  84. 40 0
      desktop/core/src/desktop/js/utils/html/stripHtmlForFunctions.ts
  85. 31 0
      desktop/core/src/desktop/js/utils/hueAnalytics.ts
  86. 0 92
      desktop/core/src/desktop/js/utils/hueUtils.test.ts
  87. 34 591
      desktop/core/src/desktop/js/utils/hueUtils.ts
  88. 6 12
      desktop/core/src/desktop/js/utils/logError.ts
  89. 4 4
      desktop/core/src/desktop/js/utils/multiLineEllipsisHandler.js
  90. 44 0
      desktop/core/src/desktop/js/utils/screen/exitFullsScreen.ts
  91. 49 0
      desktop/core/src/desktop/js/utils/screen/goFullScreen.ts
  92. 32 0
      desktop/core/src/desktop/js/utils/screen/isFullScreen.ts
  93. 31 0
      desktop/core/src/desktop/js/utils/screen/scrollbarWidth.ts
  94. 28 0
      desktop/core/src/desktop/js/utils/screen/toggleFullScreen.ts
  95. 25 0
      desktop/core/src/desktop/js/utils/string/UUID.ts
  96. 38 0
      desktop/core/src/desktop/js/utils/string/deleteAllEmptyStringKeys.ts
  97. 20 0
      desktop/core/src/desktop/js/utils/string/equalIgnoreCase.ts
  98. 32 0
      desktop/core/src/desktop/js/utils/string/parseHivePseudoJson.ts
  99. 23 0
      desktop/core/src/desktop/js/utils/timing/defer.ts
  100. 21 0
      desktop/core/src/desktop/js/utils/timing/noop.ts

+ 3 - 2
desktop/core/src/desktop/js/api/apiUtils.js

@@ -15,7 +15,8 @@
 // limitations under the License.
 
 import $ from 'jquery';
-import hueUtils from 'utils/hueUtils';
+
+import logError from 'utils/logError';
 
 /**
  * @param {Object} [response]
@@ -73,7 +74,7 @@ export const assistErrorCallback = options => {
     }
 
     if (!options || !options.silenceErrors) {
-      hueUtils.logError(errorResponse);
+      logError(errorResponse);
       if (errorMessage && errorMessage.indexOf('AuthorizationException') === -1) {
         $(document).trigger('error', errorMessage);
       }

+ 4 - 4
desktop/core/src/desktop/js/api/utils.ts

@@ -22,11 +22,11 @@ import axios, {
   CancelToken
 } from 'axios';
 import qs from 'qs';
-import huePubSub from 'utils/huePubSub';
-import { hueWindow } from '../types/types';
 
 import { CancellablePromise } from './cancellablePromise';
-import hueUtils from 'utils/hueUtils';
+import { hueWindow } from 'types/types';
+import huePubSub from 'utils/huePubSub';
+import logError from 'utils/logError';
 
 export interface DefaultApiResponse {
   status: number;
@@ -136,7 +136,7 @@ const notifyError = <T>(
   options?: Pick<ApiFetchOptions<T>, 'silenceErrors'>
 ): void => {
   if (!options || !options.silenceErrors) {
-    hueUtils.logError(response);
+    logError(response);
     if (message.indexOf('AuthorizationException') === -1) {
       huePubSub.publish('hue.error', message);
     }

+ 17 - 15
desktop/core/src/desktop/js/apps/editor/EditorViewModel.js

@@ -19,18 +19,20 @@ import * as ko from 'knockout';
 import komapping from 'knockout.mapping';
 
 import apiHelper from 'api/apiHelper';
-import ChartTransformers from 'apps/notebook/chartTransformers';
-import huePubSub from 'utils/huePubSub';
-import hueUtils from 'utils/hueUtils';
-
-import Notebook from 'apps/editor/notebook';
 import {
   ACTIVE_SNIPPET_CONNECTOR_CHANGED_EVENT,
   GET_ACTIVE_SNIPPET_CONNECTOR_EVENT
 } from 'apps/editor/events';
+import Notebook from 'apps/editor/notebook';
+import ChartTransformers from 'apps/notebook/chartTransformers';
 import { CONFIG_REFRESHED_TOPIC, GET_KNOWN_CONFIG_TOPIC } from 'config/events';
 import { findEditorConnector, getLastKnownConfig } from 'config/hueConfig';
+import huePubSub from 'utils/huePubSub';
 import { getFromLocalStorage, setInLocalStorage } from 'utils/storageUtils';
+import UUID from 'utils/string/UUID';
+import changeURL from 'utils/url/changeURL';
+import changeURLParameter from 'utils/url/changeURLParameter';
+import getParameter from 'utils/url/getParameter';
 
 export default class EditorViewModel {
   constructor(editorId, notebooks, options, CoordinatorEditorViewModel, RunningCoordinatorModel) {
@@ -99,8 +101,8 @@ export default class EditorViewModel {
 
     this.activeConnector.subscribe(connector => {
       if (connector) {
-        if (hueUtils.getParameter('type') !== connector.id) {
-          hueUtils.changeURLParameter('type', connector.id);
+        if (getParameter('type') !== connector.id) {
+          changeURLParameter('type', connector.id);
         }
         this.notifyDialectChange(connector.dialect, connector.is_sql);
       }
@@ -268,7 +270,7 @@ export default class EditorViewModel {
 
   changeURL(url) {
     if (!this.isNotificationManager()) {
-      hueUtils.changeURL(url);
+      changeURL(url);
     }
   }
 
@@ -312,10 +314,10 @@ export default class EditorViewModel {
   async init() {
     if (this.editorId) {
       await this.openNotebook(this.editorId);
-    } else if (hueUtils.getParameter('gist') !== '' || hueUtils.getParameter('type') !== '') {
-      await this.newNotebook(hueUtils.getParameter('type'));
-    } else if (hueUtils.getParameter('editor') !== '') {
-      await this.openNotebook(hueUtils.getParameter('editor'));
+    } else if (getParameter('gist') !== '' || getParameter('type') !== '') {
+      await this.newNotebook(getParameter('type'));
+    } else if (getParameter('editor') !== '') {
+      await this.openNotebook(getParameter('editor'));
     } else if (this.notebooks.length > 0) {
       this.loadNotebook(this.notebooks[0]); // Old way of loading json for /browse
     } else {
@@ -370,8 +372,8 @@ export default class EditorViewModel {
     return new Promise((resolve, reject) => {
       $.post('/notebook/api/create_notebook', {
         type: connectorId,
-        directory_uuid: hueUtils.getParameter('directory_uuid'),
-        gist: this.isNotificationManager() ? undefined : hueUtils.getParameter('gist'),
+        directory_uuid: getParameter('directory_uuid'),
+        gist: this.isNotificationManager() ? undefined : getParameter('gist'),
         blank: typeof blank !== 'undefined'
       })
         .then(data => {
@@ -451,7 +453,7 @@ export default class EditorViewModel {
 
   saveAsNotebook() {
     this.selectedNotebook().id(null);
-    this.selectedNotebook().uuid(hueUtils.UUID());
+    this.selectedNotebook().uuid(UUID());
     this.selectedNotebook().parentSavedQueryUuid(null);
     this.selectedNotebook().save(() => {
       huePubSub.publish('assist.document.refresh');

+ 5 - 4
desktop/core/src/desktop/js/apps/editor/app.js

@@ -22,10 +22,11 @@ import 'ext/bootstrap-datepicker.min';
 import 'ext/jquery.hotkeys';
 import 'jquery/plugins/jquery.hdfstree';
 
+import sqlWorkerHandler from 'sql/sqlWorkerHandler';
 import huePubSub from 'utils/huePubSub';
-import hueUtils from 'utils/hueUtils';
 import I18n from 'utils/i18n';
-import sqlWorkerHandler from 'sql/sqlWorkerHandler';
+import scrollbarWidth from 'utils/screen/scrollbarWidth';
+import waitForRendered from 'utils/timing/waitForRendered';
 
 import {
   HIDE_FIXED_HEADERS_EVENT,
@@ -360,7 +361,7 @@ huePubSub.subscribe('app.dom.loaded', app => {
     };
     $(window).data('beforeunload', window.onbeforeunload);
 
-    $('.preview-sample').css('right', 10 + hueUtils.scrollbarWidth() + 'px');
+    $('.preview-sample').css('right', 10 + scrollbarWidth() + 'px');
 
     const saveKeyHandler = () => {
       if (viewModel.canSave()) {
@@ -757,7 +758,7 @@ huePubSub.subscribe('app.dom.loaded', app => {
     huePubSub.subscribe(
       'recalculate.name.description.width',
       () => {
-        hueUtils.waitForRendered(
+        waitForRendered(
           '.editorComponents .hue-title-bar .query-name',
           el => {
             return el.is(':visible');

+ 1 - 1
desktop/core/src/desktop/js/apps/editor/components/ExecuteButton.test.ts

@@ -22,7 +22,7 @@ import { mount, shallowMount } from '@vue/test-utils';
 import Executable, { ExecutionStatus } from 'apps/editor/execution/executable';
 import sessionManager from 'apps/editor/execution/sessionManager';
 import ExecuteButton from './ExecuteButton.vue';
-import { noop } from 'utils/hueUtils';
+import noop from 'utils/timing/noop';
 
 describe('ExecuteButton.vue', () => {
   it('should render', () => {

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

@@ -56,7 +56,7 @@
   import { SqlReferenceProvider } from 'sql/reference/types';
   import { hueWindow } from 'types/types';
   import huePubSub from 'utils/huePubSub';
-  import { defer } from 'utils/hueUtils';
+  import defer from 'utils/timing/defer';
   import I18n from 'utils/i18n';
   import { getFromLocalStorage, setInLocalStorage } from 'utils/storageUtils';
 

+ 1 - 1
desktop/core/src/desktop/js/apps/editor/components/aceEditor/AceLocationHandler.ts

@@ -44,7 +44,7 @@ import {
   POST_TO_SYNTAX_WORKER_EVENT
 } from 'sql/sqlWorkerHandler';
 import { getFromLocalStorage } from 'utils/storageUtils';
-import { SqlReferenceProvider } from '../../../../sql/reference/types';
+import { SqlReferenceProvider } from 'sql/reference/types';
 
 export const REFRESH_STATEMENT_LOCATIONS_EVENT = 'editor.refresh.statement.locations';
 export const ACTIVE_STATEMENT_CHANGED_EVENT = 'editor.active.statement.changed';

+ 2 - 1
desktop/core/src/desktop/js/apps/editor/components/aceEditor/acePredict.ts

@@ -20,7 +20,8 @@ import { CancellablePromise } from 'api/cancellablePromise';
 import { PredictResponse, SqlAnalyzerProvider } from 'catalog/analyzer/types';
 import { Disposable } from 'components/utils/SubscriptionTracker';
 import { Connector } from 'config/types';
-import { defer, noop } from 'utils/hueUtils';
+import defer from 'utils/timing/defer';
+import noop from 'utils/timing/noop';
 
 type ActivePredict = { text: string; element: HTMLElement };
 

+ 10 - 14
desktop/core/src/desktop/js/apps/editor/components/aceEditor/autocomplete/AutocompleteResults.ts

@@ -49,8 +49,6 @@ import apiHelper from 'api/apiHelper';
 import dataCatalog from 'catalog/dataCatalog';
 import { SetDetails, SqlReferenceProvider, UdfDetails } from 'sql/reference/types';
 import { hueWindow } from 'types/types';
-import hueUtils from 'utils/hueUtils';
-import I18n from 'utils/i18n';
 import sqlUtils from 'sql/sqlUtils';
 import { matchesType } from 'sql/reference/typeUtils';
 import { cancelActiveRequest } from 'api/apiUtils';
@@ -61,6 +59,8 @@ import {
   getUdfsWithReturnTypes,
   getReturnTypesForUdf
 } from 'sql/reference/sqlUdfRepository';
+import I18n from 'utils/i18n';
+import equalIgnoreCase from 'utils/string/equalIgnoreCase';
 
 interface ColumnReference {
   type: string;
@@ -137,9 +137,7 @@ const PHOENIX_DIALECT = 'phoenix';
 
 const locateSubQuery = (subQueries: SubQuery[], subQueryName: string): SubQuery | undefined => {
   if (subQueries) {
-    return subQueries.find(knownSubQuery =>
-      hueUtils.equalIgnoreCase(knownSubQuery.alias, subQueryName)
-    );
+    return subQueries.find(knownSubQuery => equalIgnoreCase(knownSubQuery.alias, subQueryName));
   }
 };
 
@@ -703,7 +701,7 @@ class AutocompleteResults {
       try {
         const databases = await databasesPromise;
         const foundDb = databases.find(dbEntry =>
-          hueUtils.equalIgnoreCase(dbEntry.name, suggestTables.identifierChain[0].name)
+          equalIgnoreCase(dbEntry.name, suggestTables.identifierChain[0].name)
         );
         if (foundDb) {
           tableSuggestions = await getTableSuggestions();
@@ -799,7 +797,7 @@ class AutocompleteResults {
       return;
     }
     const cte = commonTableExpressions.find(cte =>
-      hueUtils.equalIgnoreCase(cte.alias, table.identifierChain[0].cte)
+      equalIgnoreCase(cte.alias, table.identifierChain[0].cte)
     );
     if (!cte) {
       return;
@@ -967,7 +965,7 @@ class AutocompleteResults {
               popular: false,
               weightAdjust:
                 types[0].toUpperCase() !== 'T' &&
-                types.some(type => hueUtils.equalIgnoreCase(type, childEntry.getType()))
+                types.some(type => equalIgnoreCase(type, childEntry.getType()))
                   ? 1
                   : 0,
               hasCatalogEntry: true,
@@ -1006,7 +1004,7 @@ class AutocompleteResults {
               popular: false,
               weightAdjust:
                 types[0].toUpperCase() !== 'T' &&
-                types.some(type => hueUtils.equalIgnoreCase(type, fieldType))
+                types.some(type => equalIgnoreCase(type, fieldType))
                   ? 1
                   : 0,
               hasCatalogEntry: false,
@@ -2048,7 +2046,7 @@ class AutocompleteResults {
       }
       if (
         sqlAnalyzerColumn.tableName &&
-        hueUtils.equalIgnoreCase(
+        equalIgnoreCase(
           sqlAnalyzerColumn.tableName,
           tables[i].identifierChain[tables[i].identifierChain.length - 1].name
         ) &&
@@ -2082,7 +2080,7 @@ class AutocompleteResults {
     tables.forEach(table => {
       tablesHasDefaultDatabase =
         tablesHasDefaultDatabase ||
-        hueUtils.equalIgnoreCase(
+        equalIgnoreCase(
           table.identifierChain[0].name.toLowerCase(),
           this.activeDatabase.toLowerCase()
         );
@@ -2255,9 +2253,7 @@ class AutocompleteResults {
         childrenPromise.then(resolve).catch(reject);
       });
 
-      const firstIsDb = databaseEntries.some(dbEntry =>
-        hueUtils.equalIgnoreCase(dbEntry.name, path[0])
-      );
+      const firstIsDb = databaseEntries.some(dbEntry => equalIgnoreCase(dbEntry.name, path[0]));
       if (!firstIsDb) {
         path.unshift(this.activeDatabase);
       }

+ 1 - 1
desktop/core/src/desktop/js/apps/editor/components/result/ResultTable.vue

@@ -65,7 +65,7 @@
   import { Column } from 'components/HueTable';
   import HueTable from 'components/HueTable.vue';
   import SubscriptionTracker from 'components/utils/SubscriptionTracker';
-  import { defer } from 'utils/hueUtils';
+  import defer from 'utils/timing/defer';
   import I18n from 'utils/i18n';
 
   export default defineComponent({

+ 1 - 1
desktop/core/src/desktop/js/apps/editor/components/resultChart/chartTransformers.js

@@ -17,7 +17,7 @@
 import $ from 'jquery';
 
 import HueColors from 'utils/hueColors';
-import { html2text } from 'utils/hueUtils';
+import html2text from 'utils/html/html2text';
 import { CHART_MAP_TYPE, CHART_SORTING } from 'apps/editor/components/resultChart/ko.resultChart';
 
 // The leaflet map can freeze the browser with numbers outside the map

+ 2 - 1
desktop/core/src/desktop/js/apps/editor/components/resultChart/ko.resultChart.js

@@ -34,7 +34,8 @@ import { CURRENT_QUERY_TAB_SWITCHED_EVENT } from 'apps/editor/snippet';
 import componentUtils from 'ko/components/componentUtils';
 import DisposableComponent from 'ko/components/DisposableComponent';
 import hueAnalytics from 'utils/hueAnalytics';
-import { defer, UUID } from 'utils/hueUtils';
+import defer from 'utils/timing/defer';
+import UUID from 'utils/string/UUID';
 import I18n from 'utils/i18n';
 
 export const RESULT_CHART_COMPONENT = 'snippet-result-chart';

+ 4 - 4
desktop/core/src/desktop/js/apps/editor/components/resultGrid/ko.resultDownloadActions.js

@@ -17,12 +17,12 @@
 import $ from 'jquery';
 import * as ko from 'knockout';
 
+import { SHOW_DOWNLOAD_RESULT_MODAL_EVENT } from 'apps/editor/components/resultGrid/ko.resultDownloadModal';
 import componentUtils from 'ko/components/componentUtils';
 import DisposableComponent from 'ko/components/DisposableComponent';
 import I18n from 'utils/i18n';
 import huePubSub from 'utils/huePubSub';
-import hueUtils from 'utils/hueUtils';
-import { SHOW_DOWNLOAD_RESULT_MODAL_EVENT } from 'apps/editor/components/resultGrid/ko.resultDownloadModal';
+import html2text from 'utils/html/html2text';
 
 export const NAME = 'result-download-actions';
 
@@ -134,7 +134,7 @@ class ResultDownloadActions extends DisposableComponent {
       this.meta().forEach((metaCol, idx) => {
         // Skip the row number column
         if (idx > 0) {
-          htmlString += `<th>${hueUtils.html2text(metaCol.name)}</th>`;
+          htmlString += `<th>${html2text(metaCol.name)}</th>`;
         }
       });
       htmlString += '</tr>';
@@ -143,7 +143,7 @@ class ResultDownloadActions extends DisposableComponent {
         row.forEach((cell, idx) => {
           // Skip the row number column
           if (idx > 0) {
-            htmlString += `<td>${hueUtils.html2text(cell)}</td>`;
+            htmlString += `<td>${html2text(cell)}</td>`;
           }
         });
         htmlString += '</tr>';

+ 1 - 1
desktop/core/src/desktop/js/apps/editor/components/resultGrid/ko.resultDownloadModal.js

@@ -20,7 +20,7 @@ import * as ko from 'knockout';
 import componentUtils from 'ko/components/componentUtils';
 import huePubSub from 'utils/huePubSub';
 import I18n from 'utils/i18n';
-import { UUID } from 'utils/hueUtils';
+import UUID from 'utils/string/UUID';
 
 export const NAME = 'download-result-modal';
 

+ 2 - 1
desktop/core/src/desktop/js/apps/editor/components/resultGrid/ko.resultGrid.js

@@ -23,7 +23,8 @@ import componentUtils from 'ko/components/componentUtils';
 import DisposableComponent from 'ko/components/DisposableComponent';
 import I18n from 'utils/i18n';
 import huePubSub from 'utils/huePubSub';
-import { defer, sleep } from 'utils/hueUtils';
+import defer from 'utils/timing/defer';
+import sleep from 'utils/timing/sleep';
 import {
   HIDE_FIXED_HEADERS_EVENT,
   REDRAW_FIXED_HEADERS_EVENT,

+ 3 - 1
desktop/core/src/desktop/js/apps/editor/components/resultGrid/ko.simpleResultGrid.js

@@ -22,7 +22,9 @@ import './ko.resultDownloadActions';
 import componentUtils from 'ko/components/componentUtils';
 import DisposableComponent from 'ko/components/DisposableComponent';
 import I18n from 'utils/i18n';
-import { defer, sleep, UUID } from 'utils/hueUtils';
+import defer from 'utils/timing/defer';
+import sleep from 'utils/timing/sleep';
+import UUID from 'utils/string/UUID';
 import {
   HIDE_FIXED_HEADERS_EVENT,
   REDRAW_FIXED_HEADERS_EVENT,

+ 1 - 1
desktop/core/src/desktop/js/apps/editor/components/sqlScratchpad/SqlScratchpad.vue

@@ -73,7 +73,7 @@
   import Spinner from 'components/Spinner.vue';
   import { findEditorConnector, getConfig } from 'config/hueConfig';
   import { Compute, Connector, Namespace } from 'config/types';
-  import { UUID } from 'utils/hueUtils';
+  import UUID from 'utils/string/UUID';
 
   export default defineComponent({
     name: 'SqlScratchpad',

+ 1 - 1
desktop/core/src/desktop/js/apps/editor/components/variableSubstitution/VariableSubstitution.vue

@@ -43,7 +43,7 @@
   import DataCatalogEntry, { FieldSourceMeta } from 'catalog/DataCatalogEntry';
   import ComboBox from 'components/ComboBox.vue';
   import { IdentifierLocation } from 'parse/types';
-  import { noop } from 'utils/hueUtils';
+  import noop from 'utils/timing/noop';
 
   const NAMED_OPTION_REGEX = /^(.+)\(([^)]+)\)$/;
   const LOCATION_VALUE_REGEX = /\${(\w+)=?([^{}]*)}/;

+ 2 - 6
desktop/core/src/desktop/js/apps/editor/execution/executable.ts

@@ -14,8 +14,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import $ from 'jquery';
-
 import {
   EXECUTABLE_TRANSITIONED_TOPIC,
   EXECUTABLE_UPDATED_TOPIC,
@@ -39,7 +37,7 @@ import ExecutionLogs, {
   ExecutionError,
   ExecutionLogsRaw
 } from 'apps/editor/execution/executionLogs';
-import { UUID } from 'utils/hueUtils';
+import UUID from 'utils/string/UUID';
 import Executor from 'apps/editor/execution/executor';
 
 /**
@@ -339,9 +337,7 @@ export default abstract class Executable {
         this.executeEnded = Date.now();
         this.setStatus(queryStatus.status);
         if (queryStatus.message) {
-          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-          // @ts-ignore
-          $.jHueNotify.error(queryStatus.message); // TODO: Inline instead of popup, e.g. ERROR_REGEX in Execute()
+          huePubSub.publish('hue.error', queryStatus.message);
         }
         break;
       default:

+ 1 - 1
desktop/core/src/desktop/js/apps/editor/execution/executionResult.ts

@@ -29,7 +29,7 @@ import { Observable } from 'knockout';
 import * as ko from 'knockout';
 
 import huePubSub from 'utils/huePubSub';
-import { sleep } from 'utils/hueUtils';
+import sleep from 'utils/timing/sleep';
 import Executable, { ExecutionStatus } from './executable';
 
 export const RESULT_TYPE = {

+ 3 - 3
desktop/core/src/desktop/js/apps/editor/notebook.js

@@ -21,7 +21,7 @@ import komapping from 'knockout.mapping';
 import apiHelper from 'api/apiHelper';
 import hueAnalytics from 'utils/hueAnalytics';
 import huePubSub from 'utils/huePubSub';
-import hueUtils from 'utils/hueUtils';
+import UUID from 'utils/string/UUID';
 import sessionManager from 'apps/editor/execution/sessionManager';
 
 import Snippet, { STATUS as SNIPPET_STATUS } from 'apps/editor/snippet';
@@ -38,7 +38,7 @@ export default class Notebook {
   constructor(vm, notebookRaw) {
     this.parentVm = vm;
     this.id = ko.observable(notebookRaw.id);
-    this.uuid = ko.observable(notebookRaw.uuid || hueUtils.UUID());
+    this.uuid = ko.observable(notebookRaw.uuid || UUID());
     this.name = ko.observable(notebookRaw.name || '');
     this.description = ko.observable(notebookRaw.description || '');
     this.type = ko.observable(notebookRaw.type || 'notebook');
@@ -112,7 +112,7 @@ export default class Notebook {
     huePubSub.subscribe(HISTORY_CLEARED_EVENT, () => {
       if (this.isHistory()) {
         this.id(null);
-        this.uuid(hueUtils.UUID());
+        this.uuid(UUID());
         this.parentVm.changeURL(this.parentVm.URLS.editor + '?type=' + this.parentVm.editorType());
       }
     });

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

@@ -39,7 +39,8 @@ import apiHelper from 'api/apiHelper';
 import Executor from 'apps/editor/execution/executor';
 import hueAnalytics from 'utils/hueAnalytics';
 import huePubSub from 'utils/huePubSub';
-import { defer, UUID } from 'utils/hueUtils';
+import defer from 'utils/timing/defer';
+import UUID from 'utils/string/UUID';
 import { getFromLocalStorage, setInLocalStorage } from 'utils/storageUtils';
 import sessionManager from 'apps/editor/execution/sessionManager';
 import SqlExecutable from 'apps/editor/execution/sqlExecutable';

+ 18 - 15
desktop/core/src/desktop/js/apps/notebook/NotebookViewModel.js

@@ -20,8 +20,6 @@ import komapping from 'knockout.mapping';
 
 import apiHelper from 'api/apiHelper';
 import ChartTransformers from 'apps/notebook/chartTransformers';
-import huePubSub from 'utils/huePubSub';
-import hueUtils from 'utils/hueUtils';
 
 import Notebook from 'apps/notebook/notebook';
 import Snippet from 'apps/notebook/snippet';
@@ -32,6 +30,11 @@ import {
 import { CONFIG_REFRESHED_TOPIC } from 'config/events';
 import { findEditorConnector, getLastKnownConfig } from 'config/hueConfig';
 import { getFromLocalStorage, setInLocalStorage } from 'utils/storageUtils';
+import huePubSub from 'utils/huePubSub';
+import changeURL from 'utils/url/changeURL';
+import changeURLParameter from 'utils/url/changeURLParameter';
+import getParameter from 'utils/url/getParameter';
+import UUID from 'utils/string/UUID';
 
 export default class NotebookViewModel {
   constructor(editor_id, notebooks, options, CoordinatorEditorViewModel, RunningCoordinatorModel) {
@@ -85,7 +88,7 @@ export default class NotebookViewModel {
       }
 
       self.editorMode(newVal !== 'notebook');
-      hueUtils.changeURLParameter('type', newVal);
+      changeURLParameter('type', newVal);
       if (self.editorMode()) {
         self.selectedNotebook().fetchHistory(); // Js error if notebook did not have snippets
       }
@@ -441,21 +444,21 @@ export default class NotebookViewModel {
 
     self.changeURL = function (url) {
       if (!self.isNotificationManager()) {
-        hueUtils.changeURL(url);
+        changeURL(url);
       }
     };
 
     self.init = function () {
       if (editor_id) {
         self.openNotebook(editor_id);
-      } else if (hueUtils.getParameter('gist') !== '') {
-        self.newNotebook(hueUtils.getParameter('type'));
-      } else if (hueUtils.getParameter('editor') !== '') {
-        self.openNotebook(hueUtils.getParameter('editor'));
+      } else if (getParameter('gist') !== '') {
+        self.newNotebook(getParameter('type'));
+      } else if (getParameter('editor') !== '') {
+        self.openNotebook(getParameter('editor'));
       } else if (notebooks.length > 0) {
         self.loadNotebook(notebooks[0]); // Old way of loading json for /browse
-      } else if (hueUtils.getParameter('type') !== '') {
-        self.newNotebook(hueUtils.getParameter('type'));
+      } else if (getParameter('type') !== '') {
+        self.newNotebook(getParameter('type'));
       } else {
         self.newNotebook();
       }
@@ -635,8 +638,8 @@ export default class NotebookViewModel {
         '/notebook/api/create_notebook',
         {
           type: connectorId,
-          directory_uuid: hueUtils.getParameter('directory_uuid'),
-          gist: self.isNotificationManager() ? undefined : hueUtils.getParameter('gist')
+          directory_uuid: getParameter('directory_uuid'),
+          gist: self.isNotificationManager() ? undefined : getParameter('gist')
         },
         data => {
           self.loadNotebook(data.notebook);
@@ -652,8 +655,8 @@ export default class NotebookViewModel {
               snippet.currentQueryTab(queryTab);
             }
             huePubSub.publish('detach.scrolls', self.selectedNotebook().snippets()[0]);
-            if (hueUtils.getParameter('type') === '') {
-              hueUtils.changeURLParameter('type', self.editorType());
+            if (getParameter('type') === '') {
+              changeURLParameter('type', self.editorType());
             }
             if (!self.isNotificationManager()) {
               huePubSub.publish(ACTIVE_SNIPPET_CONNECTOR_CHANGED_EVENT, self.activeConnector());
@@ -673,7 +676,7 @@ export default class NotebookViewModel {
 
     self.saveAsNotebook = function () {
       self.selectedNotebook().id(null);
-      self.selectedNotebook().uuid(hueUtils.UUID());
+      self.selectedNotebook().uuid(UUID());
       self.selectedNotebook().parentSavedQueryUuid(null);
       self.selectedNotebook().save(() => {
         huePubSub.publish('assist.document.refresh');

+ 16 - 17
desktop/core/src/desktop/js/apps/notebook/app.js

@@ -23,9 +23,11 @@ import 'ext/jquery.hotkeys';
 import 'jquery/plugins/jquery.hdfstree';
 
 import huePubSub from 'utils/huePubSub';
-import hueUtils from 'utils/hueUtils';
 import I18n from 'utils/i18n';
 import sqlWorkerHandler from 'sql/sqlWorkerHandler';
+import bootstrapRatios from 'utils/html/bootstrapRatios';
+import scrollbarWidth from 'utils/screen/scrollbarWidth';
+import waitForRendered from 'utils/timing/waitForRendered';
 import NotebookViewModel from './NotebookViewModel';
 import {
   ACTIVE_SNIPPET_CONNECTOR_CHANGED_EVENT,
@@ -604,7 +606,7 @@ huePubSub.subscribe('app.dom.loaded', app => {
     };
     $(window).data('beforeunload', window.onbeforeunload);
 
-    $('.preview-sample').css('right', 10 + hueUtils.scrollbarWidth() + 'px');
+    $('.preview-sample').css('right', 10 + scrollbarWidth() + 'px');
 
     const saveKeyHandler = () => {
       if (viewModel.canSave()) {
@@ -730,13 +732,13 @@ huePubSub.subscribe('app.dom.loaded', app => {
       const $snippet = $('#snippet_' + snippet.id());
       $snippet
         .find('.table-results .column-side')
-        .width(hueUtils.bootstrapRatios.span3() + '%')
-        .data('newWidth', hueUtils.bootstrapRatios.span3());
+        .width(bootstrapRatios.span3() + '%')
+        .data('newWidth', bootstrapRatios.span3());
       if (snippet.isResultSettingsVisible()) {
         $snippet
           .find('.table-results .grid-side')
-          .data('newWidth', hueUtils.bootstrapRatios.span9())
-          .width(hueUtils.bootstrapRatios.span9() + '%');
+          .data('newWidth', bootstrapRatios.span9())
+          .width(bootstrapRatios.span9() + '%');
       } else {
         $snippet.find('.table-results .grid-side').data('newWidth', 100).width('100%');
       }
@@ -753,9 +755,7 @@ huePubSub.subscribe('app.dom.loaded', app => {
         create: function () {
           const $snip = $('#snippet_' + snippet.id());
           initialPosition = $snip.find('.resize-bar').position().left;
-          $snip
-            .find('.table-results .column-side')
-            .data('newWidth', hueUtils.bootstrapRatios.span3());
+          $snip.find('.table-results .column-side').data('newWidth', bootstrapRatios.span3());
           $snip.find('.meta-filter').width($snip.find('.table-results .column-side').width() - 28);
         },
         drag: function (event, ui) {
@@ -764,9 +764,8 @@ huePubSub.subscribe('app.dom.loaded', app => {
             initialPosition = $snip.find('.resize-bar').position().left;
           }
           ui.position.left = Math.max(150, ui.position.left);
-          const newSpan3Width =
-            (ui.position.left * hueUtils.bootstrapRatios.span3()) / initialPosition;
-          const newSpan9Width = 100 - newSpan3Width - hueUtils.bootstrapRatios.margin();
+          const newSpan3Width = (ui.position.left * bootstrapRatios.span3()) / initialPosition;
+          const newSpan9Width = 100 - newSpan3Width - bootstrapRatios.margin();
           $snip
             .find('.table-results .column-side')
             .width(newSpan3Width + '%')
@@ -899,7 +898,7 @@ huePubSub.subscribe('app.dom.loaded', app => {
           $('.hue-datatable-search').hide();
         }
         if (tab === 'queryHistory') {
-          hueUtils.waitForRendered(
+          waitForRendered(
             $('#queryHistory .history-table'),
             el => {
               return el.is(':visible');
@@ -1141,7 +1140,7 @@ huePubSub.subscribe('app.dom.loaded', app => {
     huePubSub.subscribe(
       'editor.redraw.data',
       options => {
-        hueUtils.waitForRendered(
+        waitForRendered(
           '#snippet_' + options.snippet.id() + ' .resultTable',
           el => {
             return el.is(':visible');
@@ -1271,7 +1270,7 @@ huePubSub.subscribe('app.dom.loaded', app => {
     huePubSub.subscribe(
       'editor.grid.shown',
       snippet => {
-        hueUtils.waitForRendered(
+        waitForRendered(
           '#snippet_' + snippet.id() + ' .dataTables_wrapper',
           el => {
             return el.is(':visible');
@@ -1299,7 +1298,7 @@ huePubSub.subscribe('app.dom.loaded', app => {
     huePubSub.subscribe(
       'recalculate.name.description.width',
       () => {
-        hueUtils.waitForRendered(
+        waitForRendered(
           '.editorComponents .hue-title-bar .query-name',
           el => {
             return el.is(':visible');
@@ -1355,7 +1354,7 @@ huePubSub.subscribe('app.dom.loaded', app => {
             .width(
               100 -
                 $snip.find('.table-results .column-side').data('newWidth') -
-                hueUtils.bootstrapRatios.margin() +
+                bootstrapRatios.margin() +
                 '%'
             );
         } else {

+ 7 - 7
desktop/core/src/desktop/js/apps/notebook/chartTransformers.js

@@ -17,7 +17,7 @@
 import $ from 'jquery';
 
 import HueColors from 'utils/hueColors';
-import hueUtils from 'utils/hueUtils';
+import html2text from 'utils/html/html2text';
 
 const isNotNullForCharts = val => val !== 'NULL' && val !== null;
 
@@ -45,7 +45,7 @@ const pieChartTransformer = function (rawDatum) {
           val = 0;
         }
         _data.push({
-          label: hueUtils.html2text(item[_idxLabel]),
+          label: html2text(item[_idxLabel]),
           value: val,
           color: colors[cnt % colors.length],
           obj: item
@@ -132,7 +132,7 @@ const leafletMapChartTransformer = function (rawDatum) {
           _data.push({
             lat: Math.min(Math.max(MIN_LAT, item[_idxLat]), MAX_LAT),
             lng: Math.min(Math.max(MIN_LNG, item[_idxLng]), MAX_LNG),
-            label: hueUtils.html2text(item[_idxLabel]),
+            label: html2text(item[_idxLabel]),
             isHeat: rawDatum.snippet.chartMapType() === 'heat',
             intensity:
               _idxHeat > -1 ? (item[_idxHeat] * 1 != NaN ? item[_idxHeat] * 1 : null) : null,
@@ -188,7 +188,7 @@ const timelineChartTransformer = function (rawDatum) {
           if (isNotNullForCharts(item[_idxLabel]) && isNotNullForCharts(item[_idxValue])) {
             _data.push({
               series: _plottedSerie,
-              x: new Date(moment(hueUtils.html2text(item[_idxLabel])).valueOf()),
+              x: new Date(moment(html2text(item[_idxLabel])).valueOf()),
               y: item[_idxValue] * 1,
               color: colors[_plottedSerie % colors.length],
               obj: item
@@ -257,7 +257,7 @@ const multiSerieChartTransformer = function (rawDatum) {
               if (item[_idxPivot] === val) {
                 if (isNotNullForCharts(item[_idxValue]) && isNotNullForCharts(item[_idxLabel])) {
                   _data.push({
-                    x: _isXDate ? moment(item[_idxLabel]) : hueUtils.html2text(item[_idxLabel]),
+                    x: _isXDate ? moment(item[_idxLabel]) : html2text(item[_idxLabel]),
                     y: item[_idxValue] * 1,
                     color: colors[pivotCnt % colors.length],
                     obj: item
@@ -266,7 +266,7 @@ const multiSerieChartTransformer = function (rawDatum) {
               }
             });
             _datum.push({
-              key: hueUtils.html2text(val),
+              key: html2text(val),
               values: _data
             });
           });
@@ -367,7 +367,7 @@ const multiSerieChartTransformer = function (rawDatum) {
               if (isNotNullForCharts(item[_idxValue]) && isNotNullForCharts(item[_idxLabel])) {
                 _data.push({
                   series: _plottedSerie,
-                  x: _isXDate ? moment(item[_idxLabel]) : hueUtils.html2text(item[_idxLabel]),
+                  x: _isXDate ? moment(item[_idxLabel]) : html2text(item[_idxLabel]),
                   y: item[_idxValue] * 1,
                   color: colors[cnt % colors.length],
                   obj: item

+ 5 - 6
desktop/core/src/desktop/js/apps/notebook/notebook.js

@@ -19,10 +19,6 @@ import * as ko from 'knockout';
 import komapping from 'knockout.mapping';
 
 import apiHelper from 'api/apiHelper';
-import hueAnalytics from 'utils/hueAnalytics';
-import huePubSub from 'utils/huePubSub';
-import hueUtils from 'utils/hueUtils';
-
 import Session from 'apps/notebook/session';
 import Snippet from 'apps/notebook/snippet';
 import {
@@ -30,7 +26,10 @@ import {
   ASSIST_IS_DB_PANEL_READY_EVENT,
   ASSIST_SET_DATABASE_EVENT
 } from 'ko/components/assist/events';
+import hueAnalytics from 'utils/hueAnalytics';
+import huePubSub from 'utils/huePubSub';
 import { getFromLocalStorage } from 'utils/storageUtils';
+import UUID from 'utils/string/UUID';
 
 const NOTEBOOK_MAPPING = {
   ignore: [
@@ -70,7 +69,7 @@ class Notebook {
       typeof notebook.id != 'undefined' && notebook.id != null ? notebook.id : null
     );
     self.uuid = ko.observable(
-      typeof notebook.uuid != 'undefined' && notebook.uuid != null ? notebook.uuid : hueUtils.UUID()
+      typeof notebook.uuid != 'undefined' && notebook.uuid != null ? notebook.uuid : UUID()
     );
     self.name = ko.observable(
       typeof notebook.name != 'undefined' && notebook.name != null ? notebook.name : 'My Notebook'
@@ -752,7 +751,7 @@ class Notebook {
           self.history.removeAll();
           if (self.isHistory()) {
             self.id(null);
-            self.uuid(hueUtils.UUID());
+            self.uuid(UUID());
             vm.changeURL(vm.URLS.editor + '?type=' + vm.editorType());
           }
         }

+ 2 - 2
desktop/core/src/desktop/js/apps/notebook/result.js

@@ -17,7 +17,7 @@
 import $ from 'jquery';
 import * as ko from 'knockout';
 
-import hueUtils from 'utils/hueUtils';
+import UUID from 'utils/string/UUID';
 
 class Result {
   constructor(snippet, result) {
@@ -32,7 +32,7 @@ class Result {
       }
     );
     self.id = ko.observable(
-      typeof result.id != 'undefined' && result.id != null ? result.id : hueUtils.UUID()
+      typeof result.id != 'undefined' && result.id != null ? result.id : UUID()
     );
     self.type = ko.observable(
       typeof result.type != 'undefined' && result.type != null ? result.type : 'table'

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

@@ -24,7 +24,6 @@ import apiHelper from 'api/apiHelper';
 import dataCatalog from 'catalog/dataCatalog';
 import hueAnalytics from 'utils/hueAnalytics';
 import huePubSub from 'utils/huePubSub';
-import hueUtils from 'utils/hueUtils';
 import { getFromLocalStorage, setInLocalStorage } from 'utils/storageUtils';
 import Result from 'apps/notebook/result';
 import Session from 'apps/notebook/session';
@@ -44,6 +43,7 @@ import {
   CURSOR_POSITION_CHANGED_EVENT,
   REFRESH_STATEMENT_LOCATIONS_EVENT
 } from 'ko/bindings/ace/aceLocationHandler';
+import UUID from 'utils/string/UUID';
 
 const NOTEBOOK_MAPPING = {
   ignore: [
@@ -181,7 +181,7 @@ class Snippet {
     const self = this;
 
     self.id = ko.observable(
-      typeof snippet.id != 'undefined' && snippet.id != null ? snippet.id : hueUtils.UUID()
+      typeof snippet.id != 'undefined' && snippet.id != null ? snippet.id : UUID()
     );
     self.name = ko.observable(
       typeof snippet.name != 'undefined' && snippet.name != null ? snippet.name : ''

+ 6 - 4
desktop/core/src/desktop/js/apps/tableBrowser/app.js

@@ -20,9 +20,11 @@ import * as ko from 'knockout';
 import dataCatalog from 'catalog/dataCatalog';
 import huePubSub from 'utils/huePubSub';
 import MetastoreViewModel from 'apps/tableBrowser/metastoreViewModel';
-import hueUtils from 'utils/hueUtils';
 import I18n from 'utils/i18n';
 import { CONFIG_REFRESHED_TOPIC, GET_KNOWN_CONFIG_TOPIC } from 'config/events';
+import waitForRendered from 'utils/timing/waitForRendered';
+import getParameter from 'utils/url/getParameter';
+import replaceURL from 'utils/url/replaceURL';
 
 import 'components/er-diagram/webcomp';
 
@@ -76,7 +78,7 @@ huePubSub.subscribe('app.dom.loaded', app => {
       const selector = 'samplesTable';
       const bannerTopHeight = window.BANNER_TOP_HTML ? 30 : 0;
       if ($(selector).parents('.dataTables_wrapper').length === 0) {
-        hueUtils.waitForRendered(
+        waitForRendered(
           selector,
           el => el.find('td').length > 0,
           () => {
@@ -137,7 +139,7 @@ huePubSub.subscribe('app.dom.loaded', app => {
   huePubSub.publish(GET_KNOWN_CONFIG_TOPIC, configUpdated);
   huePubSub.subscribe(CONFIG_REFRESHED_TOPIC, configUpdated);
 
-  if (hueUtils.getParameter('refresh') === 'true') {
+  if (getParameter('refresh') === 'true') {
     // TODO: Use connectors in the table browser
     const connector = {
       id: viewModel.source().type,
@@ -158,7 +160,7 @@ huePubSub.subscribe('app.dom.loaded', app => {
         entry.clearCache({
           silenceErrors: true
         });
-        hueUtils.replaceURL('?');
+        replaceURL('?');
       });
   }
 });

+ 2 - 2
desktop/core/src/desktop/js/apps/tableBrowser/metastoreTable.js

@@ -19,8 +19,8 @@ import $ from 'jquery';
 import * as ko from 'knockout';
 
 import apiHelper from 'api/apiHelper';
+import deXSS from 'utils/html/deXSS';
 import huePubSub from 'utils/huePubSub';
-import hueUtils from 'utils/hueUtils';
 import MetastoreColumn from 'apps/tableBrowser/metastoreColumn';
 import MetastoreTableSamples from 'apps/tableBrowser/metastoreTableSamples';
 import MetastoreTablePartitions from 'apps/tableBrowser/metastoreTablePartitions';
@@ -107,7 +107,7 @@ class MetastoreTable {
     }
 
     this.commentWithoutNewLines = ko.pureComputed(() =>
-      this.comment() ? hueUtils.deXSS(this.comment().replace(/[\n\r]+/gi, ' ')) : ''
+      this.comment() ? deXSS(this.comment().replace(/[\n\r]+/gi, ' ')) : ''
     );
 
     this.comment.subscribe(newValue => {

+ 6 - 5
desktop/core/src/desktop/js/apps/tableBrowser/metastoreViewModel.js

@@ -23,8 +23,9 @@ import dataCatalog from 'catalog/dataCatalog';
 import { GET_KNOWN_CONFIG_TOPIC } from 'config/events';
 import { findEditorConnector } from 'config/hueConfig';
 import huePubSub from 'utils/huePubSub';
-import hueUtils from 'utils/hueUtils';
 import { getFromLocalStorage, withLocalStorage } from 'utils/storageUtils';
+import waitForRendered from 'utils/timing/waitForRendered';
+import changeURL from 'utils/url/changeURL';
 
 class MetastoreViewModel {
   /**
@@ -192,19 +193,19 @@ class MetastoreViewModel {
           params.namespace = this.source().namespace().id;
         }
         if (this.source().namespace().database() && this.source().namespace().database().table()) {
-          hueUtils.changeURL(
+          changeURL(
             prefix +
               'table/' +
               this.source().namespace().database().table().catalogEntry.path.join('/'),
             params
           );
         } else if (this.source().namespace().database()) {
-          hueUtils.changeURL(
+          changeURL(
             prefix + 'tables/' + this.source().namespace().database().catalogEntry.name,
             params
           );
         } else {
-          hueUtils.changeURL(prefix + 'databases', params);
+          changeURL(prefix + 'databases', params);
         }
       }
     });
@@ -232,7 +233,7 @@ class MetastoreViewModel {
       if (!col.table.samples.loading()) {
         $('.page-content').scrollTop(0);
         this.currentTab('sample');
-        hueUtils.waitForRendered(
+        waitForRendered(
           '#sampleTable',
           el => el.parent().hasClass('dataTables_wrapper'),
           () => {

+ 2 - 1
desktop/core/src/desktop/js/catalog/api.ts

@@ -33,7 +33,8 @@ import DataCatalogEntry, {
   SourceMeta
 } from 'catalog/DataCatalogEntry';
 import { hueWindow } from 'types/types';
-import { sleep, UUID } from 'utils/hueUtils';
+import sleep from 'utils/timing/sleep';
+import UUID from 'utils/string/UUID';
 import { Cluster, Compute, Connector, Namespace } from '../config/types';
 
 interface AnalyzeResponse {

+ 1 - 1
desktop/core/src/desktop/js/catalog/contextCatalog.ts

@@ -20,7 +20,7 @@ import { fetchClusters, fetchComputes, fetchNamespaces } from './api';
 import { Cluster, Compute, IdentifiableInterpreter, Namespace } from 'config/types';
 import huePubSub from 'utils/huePubSub';
 import { hueWindow } from 'types/types';
-import { noop } from 'utils/hueUtils';
+import noop from 'utils/timing/noop';
 import {
   CONTEXT_CATALOG_REFRESHED_TOPIC,
   NAMESPACES_REFRESHED_TOPIC,

+ 1 - 1
desktop/core/src/desktop/js/components/HueLink.vue

@@ -23,7 +23,7 @@
 <script lang="ts">
   import { defineComponent } from 'vue';
 
-  import { onHueLinkClick } from 'utils/hueUtils';
+  import onHueLinkClick from 'utils/html/onHueLinkClick';
 
   interface hueWindow {
     HUE_BASE_URL: string;

+ 5 - 4
desktop/core/src/desktop/js/components/SqlText.vue

@@ -23,7 +23,6 @@
 </template>
 
 <script lang="ts">
-  import $ from 'jquery';
   import { defineComponent } from 'vue';
 
   import { formatSql } from 'apps/editor/api';
@@ -168,7 +167,7 @@
           );
         });
 
-        const overflowStyle = this.enableOverflow ? ' overflow: initial !important;' : '';
+        const overflowStyle = this.enableOverflow ? ' overflow: auto;' : '';
         //TODO - Move inline styles to CSS class
         this.$el.innerHTML = `
           <div class="ace_editor ace-hue" style="background-color: transparent; ${overflowStyle}">
@@ -177,9 +176,11 @@
         `;
 
         if (this.enableOverflow) {
-          $(this.$el).css({ overflow: 'auto' });
+          this.$el.style.overflow = 'auto';
         }
-        $(this.$el).find('.ace_invisible_space').remove();
+        (this.$el as HTMLElement)
+          .querySelectorAll('.ace_invisible_space')
+          .forEach(el => el.remove());
       }
     }
   });

+ 1 - 1
desktop/core/src/desktop/js/components/directives/clickOutsideDirective.ts

@@ -15,7 +15,7 @@
 // limitations under the License.
 
 import { Directive } from 'vue';
-import { defer } from 'utils/hueUtils';
+import defer from 'utils/timing/defer';
 
 interface ClickOutsideHTMLElement extends HTMLElement {
   clickOutsideHandler?: (event: MouseEvent) => void;

+ 1 - 1
desktop/core/src/desktop/js/components/dropdown/DropdownDrawer.vue

@@ -38,7 +38,7 @@
     addClickOutsideHandler,
     removeClickOutsideHandler
   } from 'components/directives/clickOutsideDirective';
-  import { defer } from 'utils/hueUtils';
+  import defer from 'utils/timing/defer';
 
   const isOutsideViewport = (element: Element): { bottom: boolean; right: boolean } => {
     const clientRect = element.getBoundingClientRect();

+ 1 - 1
desktop/core/src/desktop/js/components/er-diagram/index.vue

@@ -65,7 +65,7 @@
   import { IEntity, IRelation } from './lib/interfaces';
   import { EntityTypes } from './lib/enums';
 
-  import { toggleFullScreen } from '../../utils/hueUtils';
+  import toggleFullScreen from 'utils/screen/toggleFullScreen';
 
   import TableEntity from './comps/table-entity.vue';
   import LiteralEntity from './comps/literal-entity.vue';

+ 1 - 1
desktop/core/src/desktop/js/components/sidebar/HueSidebar.vue

@@ -56,7 +56,7 @@
   import { CONFIG_REFRESHED_TOPIC } from 'config/events';
   import { getLastKnownConfig } from 'config/hueConfig';
   import huePubSub from 'utils/huePubSub';
-  import { onHueLinkClick } from 'utils/hueUtils';
+  import onHueLinkClick from 'utils/html/onHueLinkClick';
   import I18n from 'utils/i18n';
   import { getFromLocalStorage, setInLocalStorage } from 'utils/storageUtils';
 

+ 1 - 1
desktop/core/src/desktop/js/components/sidebar/SidebarBody.vue

@@ -63,7 +63,7 @@
   import SectionItem from './SectionItem.vue';
   import SpacerItem from './SpacerItem.vue';
   import { SidebarItem } from './types';
-  import { defer } from 'utils/hueUtils';
+  import defer from 'utils/timing/defer';
 
   export default defineComponent({
     name: 'SidebarBody',

+ 1 - 1
desktop/core/src/desktop/js/components/sidebar/SidebarDrawer.vue

@@ -26,7 +26,7 @@
   import { defineComponent } from 'vue';
 
   import SubscriptionTracker from 'components/utils/SubscriptionTracker';
-  import { defer } from 'utils/hueUtils';
+  import defer from 'utils/timing/defer';
 
   export default defineComponent({
     name: 'SidebarDrawer',

+ 5 - 7
desktop/core/src/desktop/js/doc/hueDocument.js

@@ -18,7 +18,8 @@ import $ from 'jquery';
 import * as ko from 'knockout';
 
 import apiHelper from 'api/apiHelper';
-import hueUtils from 'utils/hueUtils';
+import escapeOutput from 'utils/html/escapeOutput';
+import highlight from 'utils/html/highlight';
 import huePubSub from 'utils/huePubSub';
 
 export const DOCUMENT_UPDATED_EVENT = 'hue.document.updated';
@@ -94,8 +95,8 @@ class HueDocument {
     const successCallback = jsonUserGroups => {
       self.items = [];
       jsonUserGroups.users.forEach(user => {
-        const label = hueUtils.escapeOutput(self.prettifyUsername(user));
-        const highLightedLabel = hueUtils.highlight(label, request.term);
+        const label = escapeOutput(self.prettifyUsername(user));
+        const highLightedLabel = highlight(label, request.term);
         self.userMap[label] = user;
         self.items.push({
           data: {
@@ -109,10 +110,7 @@ class HueDocument {
       });
       jsonUserGroups.groups.forEach(group => {
         self.groupMap[group.name] = group;
-        const highLightedLabel = hueUtils.highlight(
-          hueUtils.escapeOutput(group.name),
-          request.term
-        );
+        const highLightedLabel = highlight(escapeOutput(group.name), request.term);
         self.items.push({
           data: {
             icon: 'fa fa-users',

+ 1 - 1
desktop/core/src/desktop/js/jquery/plugins/jquery.hdfs.autocomplete.js

@@ -15,7 +15,7 @@
 // limitations under the License.
 
 import $ from 'jquery';
-import { escapeOutput } from 'utils/hueUtils';
+import escapeOutput from 'utils/html/escapeOutput';
 import { hueLocalStorage } from 'utils/storageUtils';
 
 /*

+ 2 - 2
desktop/core/src/desktop/js/jquery/plugins/jquery.hdfstree.js

@@ -16,8 +16,8 @@
 
 import jQuery from 'jquery';
 
-import hueUtils from 'utils/hueUtils';
 import I18n from 'utils/i18n';
+import scrollbarWidth from 'utils/screen/scrollbarWidth';
 
 // TODO: don't create multiple jquery instances from chunks
 const $ = window.$ || jQuery;
@@ -113,7 +113,7 @@ Plugin.prototype.init = function (optionalPath) {
       'font-size': '16px',
       'border-bottom': '1px solid #FFF',
       'background-color': '#FFF',
-      width: 560 - hueUtils.scrollbarWidth() + 'px'
+      width: 560 - scrollbarWidth() + 'px'
     });
   }
 

+ 5 - 5
desktop/core/src/desktop/js/jquery/plugins/jquery.huedatatable.js

@@ -16,8 +16,8 @@
 
 import $ from 'jquery';
 
+import deXSS from 'utils/html/deXSS';
 import huePubSub from 'utils/huePubSub';
-import hueUtils from 'utils/hueUtils';
 
 /*
  * Extension to datatable to programmatically switch off datatable in case of huge tables
@@ -483,7 +483,7 @@ $.fn.hueDataTable = function (oInit) {
                 '<tr class="ht-visible-row ht-visible-row-' +
                 i +
                 '"><td>' +
-                hueUtils.deXSS(data[i][0]) +
+                deXSS(data[i][0]) +
                 '</td><td colspan="' +
                 (aoColumns.length - 1) +
                 '" class="stripe"></td></tr>';
@@ -502,7 +502,7 @@ $.fn.hueDataTable = function (oInit) {
                 '<tr class="ht-visible-row ht-visible-row-' +
                 i +
                 '"><td>' +
-                hueUtils.deXSS(data[i][0]) +
+                deXSS(data[i][0]) +
                 '</td><td colspan="' +
                 (aoColumns.length - 1) +
                 '" class="stripe"></td></tr>';
@@ -520,7 +520,7 @@ $.fn.hueDataTable = function (oInit) {
                     '<td ' +
                     (!aoColumns[j].bVisible ? 'style="display: none"' : '') +
                     '>' +
-                    hueUtils.deXSS(row[j]) +
+                    deXSS(row[j]) +
                     '</td>';
                 }
 
@@ -531,7 +531,7 @@ $.fn.hueDataTable = function (oInit) {
             } else {
               html =
                 '<td>' +
-                hueUtils.deXSS(data[i][0]) +
+                deXSS(data[i][0]) +
                 '</td><td colspan="' +
                 (aoColumns.length - 1) +
                 '" class="stripe"></td>';

+ 3 - 2
desktop/core/src/desktop/js/jquery/plugins/jquery.notify.js

@@ -15,7 +15,8 @@
 // limitations under the License.
 
 import $ from 'jquery';
-import hueUtils from 'utils/hueUtils';
+
+import deXSS from 'utils/html/deXSS';
 
 /*
  * jHue notify plugin
@@ -51,7 +52,7 @@ Plugin.prototype.show = function () {
   const MARGIN = 4;
 
   _this.options.message = _this.options.message.replace(/(<([^>]+)>)/gi, ''); // escape HTML messages
-  _this.options.message = hueUtils.deXSS(_this.options.message); // escape XSS messages
+  _this.options.message = deXSS(_this.options.message); // escape XSS messages
 
   if (/^(504|upstream connect error|Gateway Time-out)/.test(_this.options.message.trim())) {
     console.warn(_this.options.message);

+ 2 - 2
desktop/core/src/desktop/js/jquery/plugins/jquery.tableextender2.js

@@ -16,8 +16,8 @@
 
 import $ from 'jquery';
 
+import deXSS from 'utils/html/deXSS';
 import huePubSub from 'utils/huePubSub';
-import hueUtils from 'utils/hueUtils';
 import I18n from 'utils/i18n';
 
 const PLUGIN_NAME = 'jHueTableExtender2';
@@ -552,7 +552,7 @@ Plugin.prototype.drawLockedRow = function (rowNo, force) {
         '<td ' +
         (aoColumns && !aoColumns[idx].bVisible ? 'style="display: none"' : '') +
         '>' +
-        hueUtils.deXSS(col) +
+        deXSS(col) +
         '</td>';
     });
     $clone.html(tHtml);

+ 2 - 2
desktop/core/src/desktop/js/ko/bindings/charts/ko.pieChart.js

@@ -20,7 +20,7 @@ import * as ko from 'knockout';
 import nv from 'ext/nv.d3.1.1.15b.custom';
 
 import huePubSub from 'utils/huePubSub';
-import hueUtils from 'utils/hueUtils';
+import htmlEncode from 'utils/html/htmlEncode';
 import I18n from 'utils/i18n';
 
 ko.bindingHandlers.pieChart = {
@@ -59,7 +59,7 @@ ko.bindingHandlers.pieChart = {
               .showLabels(true)
               .showLegend(false)
               .tooltipContent((key, y) => {
-                return '<h3>' + hueUtils.htmlEncode(key) + '</h3><p>' + y + '</p>';
+                return '<h3>' + htmlEncode(key) + '</h3><p>' + y + '</p>';
               });
             _chart.noData(_data.message || I18n('No Data Available.'));
             const _d3 =

+ 2 - 2
desktop/core/src/desktop/js/ko/bindings/ko.codemirror.js

@@ -17,7 +17,7 @@
 import $ from 'jquery';
 import * as ko from 'knockout';
 
-import hueUtils from 'utils/hueUtils';
+import stripHtmlFromFunctions from 'utils/html/stripHtmlForFunctions';
 
 ko.bindingHandlers.codemirror = {
   init: function (element, valueAccessor) {
@@ -64,7 +64,7 @@ ko.bindingHandlers.codemirror = {
         }
         valueAccessor().data(_value);
         if ($('.widget-html-pill').parent().hasClass('active')) {
-          $('[contenteditable=true]').html(hueUtils.stripHtmlFromFunctions(valueAccessor().data()));
+          $('[contenteditable=true]').html(stripHtmlFromFunctions(valueAccessor().data()));
         }
       }, 100);
     });

+ 3 - 3
desktop/core/src/desktop/js/ko/bindings/ko.dockable.js

@@ -18,7 +18,7 @@ import $ from 'jquery';
 import * as ko from 'knockout';
 
 import huePubSub from 'utils/huePubSub';
-import hueUtils from 'utils/hueUtils';
+import scrollbarWidth from 'utils/screen/scrollbarWidth';
 
 ko.bindingHandlers.dockable = {
   init: function (element, valueAccessor) {
@@ -30,7 +30,7 @@ ko.bindingHandlers.dockable = {
 
     let initialTopPosition = -1;
     let initialSize = {
-      w: $(element).width() - hueUtils.scrollbarWidth(),
+      w: $(element).width() - scrollbarWidth(),
       h: $(element).outerHeight() + (options.jumpCorrection || 0)
     };
 
@@ -71,7 +71,7 @@ ko.bindingHandlers.dockable = {
     function resetInitialStyle() {
       $(element).removeAttr('style');
       initialSize = {
-        w: $(element).width() - hueUtils.scrollbarWidth(),
+        w: $(element).width() - scrollbarWidth(),
         h: $(element).outerHeight() + (options.jumpCorrection || 0)
       };
       dock();

+ 2 - 2
desktop/core/src/desktop/js/ko/bindings/ko.fileChooser.js

@@ -17,7 +17,7 @@
 import $ from 'jquery';
 import * as ko from 'knockout';
 
-import hueUtils from 'utils/hueUtils';
+import getFileBrowseButton from 'utils/html/getFileBrowseButton';
 
 ko.bindingHandlers.filechooser = {
   init: function (element, valueAccessor, allBindingsAccessor) {
@@ -70,7 +70,7 @@ ko.bindingHandlers.filechooser = {
     }
 
     $element.after(
-      hueUtils.getFileBrowseButton(
+      getFileBrowseButton(
         $element,
         true,
         valueAccessor,

+ 2 - 2
desktop/core/src/desktop/js/ko/bindings/ko.fresherEditor.js

@@ -17,7 +17,7 @@
 import $ from 'jquery';
 import * as ko from 'knockout';
 
-import hueUtils from 'utils/hueUtils';
+import stripHtmlFromFunctions from 'utils/html/stripHtmlForFunctions';
 
 ko.bindingHandlers.fresherEditor = {
   init: function (element, valueAccessor) {
@@ -67,7 +67,7 @@ ko.bindingHandlers.fresherEditor = {
     });
 
     function updateValues() {
-      $('[data-template]')[0].editor.setValue(hueUtils.stripHtmlFromFunctions(_el.html()));
+      $('[data-template]')[0].editor.setValue(stripHtmlFromFunctions(_el.html()));
       valueAccessor().data(_el.html());
     }
 

+ 3 - 3
desktop/core/src/desktop/js/ko/bindings/ko.html.js

@@ -16,7 +16,7 @@
 
 import * as ko from 'knockout';
 
-import hueUtils from 'utils/hueUtils';
+import deXSS from 'utils/html/deXSS';
 
 // we override the default html binding to prevent XSS/JS injection
 const originalHtmlBinding = ko.bindingHandlers.html;
@@ -24,13 +24,13 @@ const originalHtmlBinding = ko.bindingHandlers.html;
 ko.bindingHandlers.html = {
   init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
     const newValueAccessor = function () {
-      return hueUtils.deXSS(ko.unwrap(valueAccessor()));
+      return deXSS(ko.unwrap(valueAccessor()));
     };
     originalHtmlBinding.init(element, newValueAccessor, allBindings, viewModel, bindingContext);
   },
   update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
     const newValueAccessor = function () {
-      return hueUtils.deXSS(ko.unwrap(valueAccessor()));
+      return deXSS(ko.unwrap(valueAccessor()));
     };
     originalHtmlBinding.update(element, newValueAccessor, allBindings, viewModel, bindingContext);
   }

+ 2 - 2
desktop/core/src/desktop/js/ko/bindings/ko.logScroller.js

@@ -17,7 +17,7 @@
 import $ from 'jquery';
 import * as ko from 'knockout';
 
-import hueUtils from 'utils/hueUtils';
+import waitForRendered from 'utils/timing/waitForRendered';
 
 ko.bindingHandlers.logScroller = {
   init: function (element, valueAccessor, allBindings) {
@@ -62,7 +62,7 @@ ko.bindingHandlers.logScroller = {
         window.setTimeout(autoLogScroll, 0);
       });
     } else {
-      hueUtils.waitForRendered(
+      waitForRendered(
         element,
         el => {
           return el.is(':visible');

+ 2 - 2
desktop/core/src/desktop/js/ko/bindings/ko.toggleOverflow.js

@@ -17,11 +17,11 @@
 import $ from 'jquery';
 import * as ko from 'knockout';
 
-import hueUtils from 'utils/hueUtils';
+import isOverflowing from 'utils/html/isOverflowing';
 
 ko.bindingHandlers.toggleOverflow = {
   render: function ($element, options) {
-    if (hueUtils.isOverflowing($element.find('.toggle-overflow'))) {
+    if (isOverflowing($element.find('.toggle-overflow'))) {
       $('<div>')
         .addClass('toggle-overflow-toggle')
         .html('<i class="fa fa-caret-down muted"></i>')

+ 4 - 3
desktop/core/src/desktop/js/ko/bindings/ko.toggleOverflow.test.js

@@ -17,9 +17,10 @@
 import 'ext/bootstrap.2.3.2.min';
 
 import { koSetup } from 'jest/koTestUtils';
-
+import isOverflowing from 'utils/html/isOverflowing';
 import './ko.toggleOverflow';
-import hueUtils from 'utils/hueUtils';
+
+jest.mock('utils/html/isOverflowing');
 
 describe('ko.toggleOverflow.js', () => {
   const setup = koSetup();
@@ -27,7 +28,7 @@ describe('ko.toggleOverflow.js', () => {
   jest.useFakeTimers();
 
   it('should render binding', async () => {
-    jest.spyOn(hueUtils, 'isOverflowing').mockImplementation(() => true);
+    isOverflowing.mockReturnValue(true);
 
     const wrapper = await setup.renderKo(`<div data-bind="toggleOverflow">Some text</div>`, {});
 

+ 3 - 2
desktop/core/src/desktop/js/ko/bindings/ko.tooltip.js

@@ -16,8 +16,9 @@
 
 import $ from 'jquery';
 import * as ko from 'knockout';
+
 import { registerBinding } from './bindingUtils';
-import hueUtils from 'utils/hueUtils';
+import escapeOutput from 'utils/html/escapeOutput';
 
 export const NAME = 'tooltip';
 
@@ -36,7 +37,7 @@ registerBinding(NAME, {
     if (options.title) {
       const title = ko.unwrap(options.title); // Not always an observable
       if (typeof title === 'string' && !options.html) {
-        options.title = hueUtils.escapeOutput(title);
+        options.title = escapeOutput(title);
       }
     }
 

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

@@ -19,7 +19,7 @@ import * as apiUtils from 'api/utils';
 import * as refApiUtils from 'sql/reference/apiUtils';
 import AssistFunctionsPanel from './ko.assistFunctionsPanel';
 import { refreshConfig } from 'config/hueConfig';
-import { sleep } from 'utils/hueUtils';
+import sleep from 'utils/timing/sleep';
 
 describe('ko.assistFunctionsPanel.js', () => {
   jest.spyOn(refApiUtils, 'fetchUdfs').mockImplementation(() => Promise.resolve([]));

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

@@ -19,7 +19,7 @@ import * as ko from 'knockout';
 import * as apiUtils from 'api/utils';
 import AssistLangRefPanel from './ko.assistLangRefPanel';
 import { refreshConfig } from 'config/hueConfig';
-import { sleep } from 'utils/hueUtils';
+import sleep from 'utils/timing/sleep';
 
 describe('ko.assistLangRefPanel.js', () => {
   beforeAll(() => {

+ 1 - 1
desktop/core/src/desktop/js/ko/components/ko.catalogEntriesList.js

@@ -21,7 +21,7 @@ import * as ko from 'knockout';
 import { ASSIST_KEY_COMPONENT } from './assist/ko.assistKey';
 import componentUtils from './componentUtils';
 import huePubSub from 'utils/huePubSub';
-import { noop } from 'utils/hueUtils';
+import noop from 'utils/timing/noop';
 import I18n from 'utils/i18n';
 
 export const NAME = 'catalog-entries-list';

+ 1 - 1
desktop/core/src/desktop/js/ko/components/ko.editorDroppableMenu.js

@@ -23,7 +23,7 @@ import sqlUtils from 'sql/sqlUtils';
 import DisposableComponent from './DisposableComponent';
 import { DRAGGABLE_TEXT_META_EVENT } from 'ko/bindings/ko.draggableText';
 import { INSERT_AT_CURSOR_EVENT } from 'ko/bindings/ace/ko.aceEditor';
-import { sleep } from 'utils/hueUtils';
+import sleep from 'utils/timing/sleep';
 
 export const NAME = 'hue-editor-droppable-menu';
 

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

@@ -18,7 +18,7 @@ import * as ko from 'knockout';
 
 import componentUtils from './componentUtils';
 import huePubSub from 'utils/huePubSub';
-import hueUtils from 'utils/hueUtils';
+import html2text from 'utils/html/html2text';
 import I18n from 'utils/i18n';
 
 export const NAME = 'field-samples';
@@ -113,7 +113,7 @@ class FieldSamples {
         self.catalogEntry().getDialect() === 'hive');
 
     self.sampleClick = function (html) {
-      self.onSampleClick(hueUtils.html2text(html));
+      self.onSampleClick(html2text(html));
       huePubSub.publish('context.popover.hide');
     };
 

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

@@ -20,7 +20,7 @@ import koMapping from 'knockout.mapping';
 
 import componentUtils from './componentUtils';
 import I18n from 'utils/i18n';
-import hueUtils from 'utils/hueUtils';
+import UUID from 'utils/string/UUID';
 
 export const NAME = 'hue-sentry-privileges';
 
@@ -625,7 +625,7 @@ const _create_ko_privilege = privilege =>
     timestamp: privilege.timestamp,
     roleName: privilege.roleName,
     grantOption: privilege.grantOption,
-    id: hueUtils.UUID()
+    id: UUID()
   });
 
 class SentryPrivilegesViewModel {

+ 1 - 1
desktop/core/src/desktop/js/ko/components/simpleAceEditor/ko.simpleAceEditor.js

@@ -20,7 +20,7 @@ import ace from 'ext/aceHelper';
 
 import AceLocationHandler from 'ko/bindings/ace/aceLocationHandler';
 import componentUtils from 'ko/components/componentUtils';
-import { UUID } from 'utils/hueUtils';
+import UUID from 'utils/string/UUID';
 import { hueLocalStorage } from 'utils/storageUtils';
 import SolrFormulaAutocompleter from './solrFormulaAutocompleter';
 import SolrQueryAutocompleter from './solrQueryAutocompleter';

+ 5 - 12
desktop/core/src/desktop/js/nvd3/nv.d3.lineWithBrushChart.js

@@ -16,7 +16,7 @@
 
 import d3v3 from 'd3v3';
 
-import hueUtils from 'utils/hueUtils';
+import htmlEncode from 'utils/html/htmlEncode';
 import nv from 'ext/nv.d3.1.1.15b.custom';
 
 nv.models.lineWithBrushChart = function () {
@@ -50,19 +50,12 @@ nv.models.lineWithBrushChart = function () {
     tooltip = null,
     displayValuesInLegend = false;
   const tooltipSimple = function (value) {
-      return (
-        '<h3>' +
-        hueUtils.htmlEncode(value.key) +
-        '</h3>' +
-        '<p>' +
-        hueUtils.htmlEncode(value.x) +
-        '</p>'
-      );
+      return '<h3>' + htmlEncode(value.key) + '</h3>' + '<p>' + htmlEncode(value.x) + '</p>';
     },
     tooltipMultiple = function (values) {
       return (
         '<h3>' +
-        hueUtils.htmlEncode(values[0] && values[0].x) +
+        htmlEncode(values[0] && values[0].x) +
         '</h3>' +
         values
           .map(value => {
@@ -70,9 +63,9 @@ nv.models.lineWithBrushChart = function () {
               '<p><span class="circle" style="background-color:' +
               value.color +
               '"></span><b>' +
-              hueUtils.htmlEncode(value.key) +
+              htmlEncode(value.key) +
               '</b> ' +
-              hueUtils.htmlEncode(value.y) +
+              htmlEncode(value.y) +
               '</p>'
             );
           })

+ 5 - 12
desktop/core/src/desktop/js/nvd3/nv.d3.multiBarWithBrushChart.js

@@ -16,7 +16,7 @@
 
 import d3v3 from 'd3v3';
 
-import hueUtils from 'utils/hueUtils';
+import htmlEncode from 'utils/html/htmlEncode';
 import nv from 'ext/nv.d3.1.1.15b.custom';
 
 nv.models.multiBarWithBrushChart = function () {
@@ -60,19 +60,12 @@ nv.models.multiBarWithBrushChart = function () {
     defaultState = null,
     noData = 'No Data Available.';
   const tooltipSimple = function (value) {
-      return (
-        '<h3>' +
-        hueUtils.htmlEncode(value.key) +
-        '</h3>' +
-        '<p>' +
-        hueUtils.htmlEncode(value.x) +
-        '</p>'
-      );
+      return '<h3>' + htmlEncode(value.key) + '</h3>' + '<p>' + htmlEncode(value.x) + '</p>';
     },
     tooltipMultiple = function (values) {
       return (
         '<h3>' +
-        hueUtils.htmlEncode(values[0] && values[0].x) +
+        htmlEncode(values[0] && values[0].x) +
         '</h3>' +
         values
           .map(value => {
@@ -80,9 +73,9 @@ nv.models.multiBarWithBrushChart = function () {
               '<p><span class="circle" style="background-color:' +
               value.color +
               '"></span><b>' +
-              hueUtils.htmlEncode(value.key) +
+              htmlEncode(value.key) +
               '</b> ' +
-              hueUtils.htmlEncode(value.y) +
+              htmlEncode(value.y) +
               '</p>'
             );
           })

+ 13 - 10
desktop/core/src/desktop/js/onePageViewModel.js

@@ -20,9 +20,12 @@ import * as ko from 'knockout';
 import page from 'page';
 
 import { CONFIG_REFRESHED_TOPIC, GET_KNOWN_CONFIG_TOPIC } from 'config/events';
-import hueUtils from 'utils/hueUtils';
 import huePubSub from 'utils/huePubSub';
 import I18n from 'utils/i18n';
+import waitForObservable from 'utils/timing/waitForObservable';
+import waitForVariable from 'utils/timing/waitForVariable';
+import getParameter from 'utils/url/getParameter';
+import getSearchParameter from 'utils/url/getSearchParameter';
 
 class OnePageViewModel {
   constructor() {
@@ -52,7 +55,7 @@ class OnePageViewModel {
     self.changeEditorType = function (type) {
       self.getActiveAppViewModel(viewModel => {
         if (viewModel && viewModel.selectedNotebook) {
-          hueUtils.waitForObservable(viewModel.selectedNotebook, () => {
+          waitForObservable(viewModel.selectedNotebook, () => {
             if (viewModel.editorType() !== type) {
               viewModel.selectedNotebook().selectedSnippet(type);
               if (!window.ENABLE_NOTEBOOK_2) {
@@ -101,17 +104,17 @@ class OnePageViewModel {
     huePubSub.subscribe('open.importer.query', data => {
       self.loadApp('importer');
       self.getActiveAppViewModel(viewModel => {
-        hueUtils.waitForVariable(viewModel.createWizard, () => {
-          hueUtils.waitForVariable(viewModel.createWizard.prefill, () => {
+        waitForVariable(viewModel.createWizard, () => {
+          waitForVariable(viewModel.createWizard.prefill, () => {
             viewModel.createWizard.prefill.source_type(data['source_type']);
             viewModel.createWizard.prefill.target_type(data['target_type']);
             viewModel.createWizard.prefill.target_path(data['target_path']);
             viewModel.createWizard.destination.outputFormat(data['target_type']);
           });
-          hueUtils.waitForVariable(viewModel.createWizard.source.query, () => {
+          waitForVariable(viewModel.createWizard.source.query, () => {
             viewModel.createWizard.source.query({ id: data.id, name: data.name });
           });
-          hueUtils.waitForVariable(viewModel.createWizard.loadSampleData, () => {
+          waitForVariable(viewModel.createWizard.loadSampleData, () => {
             viewModel.createWizard.loadSampleData(data);
           });
         });
@@ -470,7 +473,7 @@ class OnePageViewModel {
 
     page.base(window.HUE_BASE_URL + '/hue');
 
-    const getUrlParameter = name => hueUtils.getParameter(name) || '';
+    const getUrlParameter = name => getParameter(name) || '';
 
     self.lastContext = null;
 
@@ -622,8 +625,8 @@ class OnePageViewModel {
             if (!_params) {
               console.warn('Could not match ' + ctx.path);
             }
-            hueUtils.waitForVariable(viewModel.createWizard, () => {
-              hueUtils.waitForVariable(viewModel.createWizard.prefill, () => {
+            waitForVariable(viewModel.createWizard, () => {
+              waitForVariable(viewModel.createWizard.prefill, () => {
                 viewModel.createWizard.prefill.source_type(_params && _params[1] ? _params[1] : '');
                 viewModel.createWizard.prefill.target_type(_params && _params[2] ? _params[2] : '');
                 viewModel.createWizard.prefill.target_path(_params && _params[3] ? _params[3] : '');
@@ -661,7 +664,7 @@ class OnePageViewModel {
         url: '/notebook',
         app: function (ctx) {
           self.loadApp('notebook');
-          const notebookId = hueUtils.getSearchParameter('?' + ctx.querystring, 'notebook');
+          const notebookId = getSearchParameter('?' + ctx.querystring, 'notebook');
           if (notebookId !== '') {
             self.getActiveAppViewModel(viewModel => {
               self.isLoadingEmbeddable(true);

+ 1 - 1
desktop/core/src/desktop/js/sidePanelViewModel.js

@@ -22,7 +22,7 @@ import huePubSub from 'utils/huePubSub';
 import { ACTIVE_SNIPPET_CONNECTOR_CHANGED_EVENT } from 'apps/editor/events';
 import { SHOW_LEFT_ASSIST_EVENT, SHOW_RIGHT_ASSIST_EVENT } from 'ko/components/assist/events';
 import { getFromLocalStorage, setInLocalStorage } from 'utils/storageUtils';
-import { defer } from './utils/hueUtils';
+import defer from 'utils/timing/defer';
 
 class SidePanelViewModel {
   constructor() {

+ 11 - 13
desktop/core/src/desktop/js/sql/autocompleteResults.js

@@ -21,7 +21,6 @@ import apiHelper from 'api/apiHelper';
 import sqlAnalyzerRepository from 'catalog/analyzer/sqlAnalyzerRepository';
 import dataCatalog from 'catalog/dataCatalog';
 import HueColors from 'utils/hueColors';
-import hueUtils from 'utils/hueUtils';
 import huePubSub from 'utils/huePubSub';
 import I18n from 'utils/i18n';
 import sqlUtils from 'sql/sqlUtils';
@@ -29,6 +28,7 @@ import { matchesType } from 'sql/reference/typeUtils';
 import { DIALECT } from 'apps/editor/snippet';
 import { cancelActiveRequest } from 'api/apiUtils';
 import { findBrowserConnector, getRootFilePath } from 'config/hueConfig';
+import equalIgnoreCase from 'utils/string/equalIgnoreCase';
 import {
   findUdf,
   getArgumentDetailsForUdf,
@@ -237,9 +237,9 @@ const locateSubQuery = function (subQueries, subQueryName) {
   if (typeof subQueries === 'undefined') {
     return null;
   }
-  const foundSubQueries = subQueries.filter(knownSubQuery => {
-    return hueUtils.equalIgnoreCase(knownSubQuery.alias, subQueryName);
-  });
+  const foundSubQueries = subQueries.filter(knownSubQuery =>
+    equalIgnoreCase(knownSubQuery.alias, subQueryName)
+  );
   if (foundSubQueries.length > 0) {
     return foundSubQueries[0];
   }
@@ -944,7 +944,7 @@ class AutocompleteResults {
       try {
         const databases = await databasesPromise;
         const foundDb = databases.find(dbEntry =>
-          hueUtils.equalIgnoreCase(dbEntry.name, suggestTables.identifierChain[0].name)
+          equalIgnoreCase(dbEntry.name, suggestTables.identifierChain[0].name)
         );
         if (foundDb) {
           tableSuggestions = await getTableSuggestions();
@@ -1032,7 +1032,7 @@ class AutocompleteResults {
 
   async addCteColumns(table, columnSuggestions) {
     const cte = this.parseResult.commonTableExpressions.find(cte =>
-      hueUtils.equalIgnoreCase(cte.alias, table.identifierChain[0].cte)
+      equalIgnoreCase(cte.alias, table.identifierChain[0].cte)
     );
     if (!cte) {
       return;
@@ -1180,7 +1180,7 @@ class AutocompleteResults {
               popular: ko.observable(false),
               weightAdjust:
                 types[0].toUpperCase() !== 'T' &&
-                types.some(type => hueUtils.equalIgnoreCase(type, childEntry.getType()))
+                types.some(type => equalIgnoreCase(type, childEntry.getType()))
                   ? 1
                   : 0,
               hasCatalogEntry: true,
@@ -1219,7 +1219,7 @@ class AutocompleteResults {
               popular: ko.observable(false),
               weightAdjust:
                 types[0].toUpperCase() !== 'T' &&
-                types.some(type => hueUtils.equalIgnoreCase(type, fieldType))
+                types.some(type => equalIgnoreCase(type, fieldType))
                   ? 1
                   : 0,
               hasCatalogEntry: false,
@@ -2254,7 +2254,7 @@ class AutocompleteResults {
       }
       if (
         sqlAnalyzerColumn.tableName &&
-        hueUtils.equalIgnoreCase(
+        equalIgnoreCase(
           sqlAnalyzerColumn.tableName,
           tables[i].identifierChain[tables[i].identifierChain.length - 1].name
         ) &&
@@ -2285,7 +2285,7 @@ class AutocompleteResults {
     tables.forEach(table => {
       tablesHasDefaultDatabase =
         tablesHasDefaultDatabase ||
-        hueUtils.equalIgnoreCase(
+        equalIgnoreCase(
           table.identifierChain[0].name.toLowerCase(),
           this.activeDatabase.toLowerCase()
         );
@@ -2443,9 +2443,7 @@ class AutocompleteResults {
         childrenPromise.then(resolve).catch(reject);
       });
 
-      const firstIsDb = databaseEntries.some(dbEntry =>
-        hueUtils.equalIgnoreCase(dbEntry.name, path[0])
-      );
+      const firstIsDb = databaseEntries.some(dbEntry => equalIgnoreCase(dbEntry.name, path[0]));
       if (!firstIsDb) {
         path.unshift(this.activeDatabase);
       }

+ 2 - 2
desktop/core/src/desktop/js/sql/autocompleteResults.test.js

@@ -19,7 +19,7 @@ import $ from 'jquery';
 import ApiHelper from 'api/apiHelper';
 import * as CatalogApi from 'catalog/api';
 import * as apiUtils from 'sql/reference/apiUtils';
-import { CancellablePromise } from '../api/cancellablePromise';
+import { CancellablePromise } from 'api/cancellablePromise';
 import AutocompleteResults from './autocompleteResults';
 import dataCatalog from 'catalog/dataCatalog';
 import huePubSub from 'utils/huePubSub';
@@ -27,7 +27,7 @@ import I18n from 'utils/i18n';
 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/hueUtils';
+import sleep from 'utils/timing/sleep';
 
 describe('AutocompleteResults.js', () => {
   const failResponse = {

+ 1 - 3
desktop/core/src/desktop/js/sql/sqlWorkerHandler.js

@@ -14,8 +14,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import $ from 'jquery';
-
 import huePubSub from 'utils/huePubSub';
 import { resolveCatalogEntry } from 'sql/sqlUtils';
 import { applyCancellable } from '../catalog/catalogUtils';
@@ -37,7 +35,7 @@ const attachEntryResolver = function (location, connector, namespace, compute) {
 
     if (!location.identifierChain && !location.colRef && !location.colRef.identifierChain) {
       if (!location.resolvePathPromise) {
-        location.resolvePathPromise = $.Deferred().reject().promise();
+        location.resolvePathPromise = Promise.reject();
       }
       return location.resolvePathPromise;
     }

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

@@ -64,4 +64,5 @@ export interface hueWindow {
   WS_CHANNEL?: string;
   hueDebug?: HueDebug;
   DISABLE_LOCAL_STORAGE?: boolean;
+  trackOnGA?(track: string): void;
 }

+ 41 - 0
desktop/core/src/desktop/js/utils/html/bootstrapRatios.ts

@@ -0,0 +1,41 @@
+// 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.
+
+const bootstrapRatios = {
+  span3(): number {
+    if (window.innerWidth >= 1200) {
+      return 23.07692308;
+    } else if (window.innerWidth >= 768 && window.innerWidth <= 979) {
+      return 22.9281768;
+    } else {
+      return 23.17073171;
+    }
+  },
+  span9(): number {
+    if (window.innerWidth >= 1200) {
+      return 74.35897436;
+    } else if (window.innerWidth >= 768 && window.innerWidth <= 979) {
+      return 74.30939227;
+    } else {
+      return 74.3902439;
+    }
+  },
+  margin(): number {
+    return 2.56410256;
+  }
+};
+
+export default bootstrapRatios;

+ 69 - 0
desktop/core/src/desktop/js/utils/html/deXSS.test.ts

@@ -0,0 +1,69 @@
+// 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 deXSS from './deXSS';
+
+describe('deXSS.ts', () => {
+  it('should return empty string when the value is undefined', () => {
+    expect(deXSS(undefined)).toEqual('');
+  });
+
+  it('should return false when the value is false', () => {
+    expect(deXSS(false)).toEqual('false');
+  });
+
+  it('should return 0 when the value is 0', () => {
+    expect(deXSS(0)).toEqual('0');
+  });
+
+  it('should return null when the value is null', () => {
+    expect(deXSS(null)).toEqual('null');
+  });
+
+  it('should remove JS code from a string', () => {
+    expect(deXSS('hello <script>alert(123)</script>world')).toEqual('hello world');
+  });
+
+  it('Should not remove strong tag from a string', () => {
+    expect(deXSS('<strong>Hello World</strong>')).toEqual('<strong>Hello World</strong>');
+  });
+
+  it('Should remove img tag from a string', () => {
+    expect(deXSS('<img src=asf onerror=alert(document.cookie) />hello')).toEqual('hello');
+  });
+
+  it('Should not remove p tag from a string', () => {
+    expect(deXSS('<p> Hello World </p>')).toEqual('<p> Hello World </p>');
+  });
+
+  it('Should not remove h1 tag from a string', () => {
+    expect(deXSS('<h1> Hello World </h1>')).toEqual('<h1> Hello World </h1>');
+  });
+
+  it('should preserve entities as such', () => {
+    expect(deXSS('<a name="&lt;silly&gt;">&lt;Kapow!&gt;</a>')).toEqual(
+      '<a name="&lt;silly&gt;">&lt;Kapow!&gt;</a>'
+    );
+  });
+
+  it('should dump comments', () => {
+    expect(deXSS('<p><!-- Blah blah -->Whee</p>')).toEqual('<p>Whee</p>');
+  });
+
+  it('should dump an uppercase javascript url', () => {
+    expect(deXSS('<a href="JAVASCRIPT:alert(\'foo\')">Hax</a>')).toEqual('<a>Hax</a>');
+  });
+});

+ 22 - 0
desktop/core/src/desktop/js/utils/html/deXSS.ts

@@ -0,0 +1,22 @@
+// 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 sanitizeHtml from 'sanitize-html';
+
+const deXSS = (str?: boolean | string | number | null): string =>
+  (typeof str !== 'undefined' && sanitizeHtml(str as string)) || '';
+
+export default deXSS;

+ 21 - 0
desktop/core/src/desktop/js/utils/html/escapeOutput.ts

@@ -0,0 +1,21 @@
+// 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 htmlEncode from './htmlEncode';
+
+const escapeOutput = (str: string): string => htmlEncode(str).trim();
+
+export default escapeOutput;

+ 193 - 0
desktop/core/src/desktop/js/utils/html/getFileBrowseButton.ts

@@ -0,0 +1,193 @@
+// 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';
+
+type ComplexValueAccessor = () => {
+  value?: KnockoutObservable<string>;
+  displayJustLastBit?: boolean;
+};
+
+const getFileBrowseButton = (
+  inputElement: JQuery<HTMLInputElement>,
+  selectFolder: string,
+  valueAccessor: KnockoutObservable<string> | ComplexValueAccessor,
+  stripHdfsPrefix: boolean,
+  allBindingsAccessor: KnockoutAllBindingsAccessor,
+  isAddon: boolean,
+  isNestedModal: boolean,
+  linkMarkup: boolean
+): JQuery => {
+  let button: JQuery;
+  if (isAddon) {
+    button = $('<span>').addClass('add-on muted pointer filechooser-clickable').text('..');
+  } else if (linkMarkup) {
+    button = $('<a>').addClass('btn').addClass('fileChooserBtn filechooser-clickable').text('..');
+  } else {
+    button = $('<button>')
+      .addClass('btn')
+      .addClass('fileChooserBtn filechooser-clickable')
+      .text('..');
+  }
+  button.on('click', e => {
+    e.preventDefault();
+    if (!isNestedModal) {
+      $('body').addClass('modal-open');
+    }
+
+    function callFileChooser() {
+      let initialPath: string =
+        (<string>inputElement.val()).trim() !== '' ? <string>inputElement.val() || '' : '/';
+      if (
+        (allBindingsAccessor &&
+          allBindingsAccessor().filechooserOptions &&
+          allBindingsAccessor().filechooserOptions.skipInitialPathIfEmpty &&
+          inputElement.val() === '') ||
+        (allBindingsAccessor && allBindingsAccessor().filechooserPrefixSeparator)
+      ) {
+        initialPath = '';
+      }
+      if (inputElement.data('fullPath')) {
+        initialPath = inputElement.data('fullPath');
+      }
+      if (initialPath.indexOf('hdfs://') > -1) {
+        initialPath = initialPath.substring(7);
+      }
+
+      let supportSelectFolder = !!selectFolder;
+      if (
+        allBindingsAccessor &&
+        typeof allBindingsAccessor().filechooserOptions !== 'undefined' &&
+        typeof allBindingsAccessor().filechooserOptions.selectFolder !== 'undefined'
+      ) {
+        supportSelectFolder = allBindingsAccessor().filechooserOptions.selectFolder;
+      }
+
+      $('#filechooser').jHueFileChooser({
+        suppressErrors: true,
+        selectFolder: supportSelectFolder,
+        onFolderChoose: filePath => {
+          handleChoice(filePath, stripHdfsPrefix);
+          if (selectFolder) {
+            $('#chooseFile').modal('hide');
+            if (!isNestedModal) {
+              $('.modal-backdrop').remove();
+            }
+          }
+        },
+        onFileChoose: filePath => {
+          handleChoice(filePath, stripHdfsPrefix);
+          $('#chooseFile').modal('hide');
+          if (!isNestedModal) {
+            $('.modal-backdrop').remove();
+          }
+        },
+        createFolder:
+          allBindingsAccessor &&
+          allBindingsAccessor().filechooserOptions &&
+          allBindingsAccessor().filechooserOptions.createFolder,
+        uploadFile:
+          allBindingsAccessor &&
+          allBindingsAccessor().filechooserOptions &&
+          allBindingsAccessor().filechooserOptions.uploadFile,
+        initialPath: initialPath,
+        errorRedirectPath: '',
+        forceRefresh: true,
+        showExtraHome:
+          allBindingsAccessor &&
+          allBindingsAccessor().filechooserOptions &&
+          allBindingsAccessor().filechooserOptions.showExtraHome,
+        extraHomeProperties:
+          allBindingsAccessor &&
+          allBindingsAccessor().filechooserOptions &&
+          allBindingsAccessor().filechooserOptions.extraHomeProperties
+            ? allBindingsAccessor().filechooserOptions.extraHomeProperties
+            : {},
+        filterExtensions:
+          allBindingsAccessor && allBindingsAccessor().filechooserFilter
+            ? allBindingsAccessor().filechooserFilter
+            : '',
+        displayOnlyFolders:
+          allBindingsAccessor &&
+          allBindingsAccessor().filechooserOptions &&
+          allBindingsAccessor().filechooserOptions.displayOnlyFolders
+      });
+      $('#chooseFile').modal('show');
+      if (!isNestedModal) {
+        $('#chooseFile').on('hidden', () => {
+          $('body').removeClass('modal-open');
+          $('.modal-backdrop').remove();
+        });
+      }
+    }
+
+    // check if it's a relative path
+    callFileChooser();
+
+    const handleChoice = (filePath: string, stripHdfsPrefix: boolean) => {
+      if (allBindingsAccessor && allBindingsAccessor().filechooserPrefixSeparator) {
+        filePath =
+          (<string>inputElement.val()).split(allBindingsAccessor().filechooserPrefixSeparator)[0] +
+          '=' +
+          filePath;
+      }
+      if (
+        allBindingsAccessor &&
+        allBindingsAccessor().filechooserOptions &&
+        allBindingsAccessor().filechooserOptions.deploymentDir
+      ) {
+        inputElement.data('fullPath', filePath);
+        inputElement.attr('data-original-title', filePath);
+        if (filePath.indexOf(allBindingsAccessor().filechooserOptions.deploymentDir) === 0) {
+          filePath = filePath.substr(
+            allBindingsAccessor().filechooserOptions.deploymentDir.length + 1
+          );
+        }
+      }
+      if (stripHdfsPrefix) {
+        inputElement.val(filePath);
+      } else {
+        inputElement.val('hdfs://' + filePath);
+      }
+      inputElement.trigger('change');
+      if (valueAccessor) {
+        if (
+          typeof valueAccessor() === 'function' ||
+          typeof (<ComplexValueAccessor>valueAccessor)().value === 'function'
+        ) {
+          const complex = (<ComplexValueAccessor>valueAccessor)();
+          if (complex.value) {
+            complex.value(<string>inputElement.val());
+            if (complex.displayJustLastBit) {
+              inputElement.data('fullPath', <string>inputElement.val());
+              inputElement.attr('data-original-title', <string>inputElement.val());
+              const value = <string>inputElement.val();
+              inputElement.val(value.split('/')[value.split('/').length - 1]);
+            }
+            return;
+          }
+        }
+        (<KnockoutObservable<string>>valueAccessor())(<string>inputElement.val());
+      }
+    };
+  });
+  if (allBindingsAccessor && allBindingsAccessor().filechooserDisabled) {
+    button.addClass('disabled').attr('disabled', 'disabled');
+  }
+  return button;
+};
+
+export default getFileBrowseButton;

+ 30 - 0
desktop/core/src/desktop/js/utils/html/getStyleFromCSSClass.ts

@@ -0,0 +1,30 @@
+// 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.
+
+const getStyleFromCSSClass = (cssClass: string): CSSStyleDeclaration | CSSRule | undefined => {
+  for (let i = 0; i < document.styleSheets.length; i++) {
+    const cssClasses = document.styleSheets[i].rules || document.styleSheets[i].cssRules;
+    for (let x = 0; x < cssClasses.length; x++) {
+      if ((<CSSStyleRule>cssClasses[x]).selectorText === cssClass) {
+        return (<CSSStyleRule>cssClasses[x]).style
+          ? (<CSSStyleRule>cssClasses[x]).style
+          : <CSSStyleRule>cssClasses[x];
+      }
+    }
+  }
+};
+
+export default getStyleFromCSSClass;

+ 44 - 0
desktop/core/src/desktop/js/utils/html/highlight.ts

@@ -0,0 +1,44 @@
+// 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.
+
+const highlight = (text: string, searchTerm: string): string => {
+  if (searchTerm === '' || text === '') {
+    return text;
+  }
+
+  let remText = text;
+  let highLightedText = '';
+  searchTerm = searchTerm.toLowerCase();
+
+  let startIndex;
+  do {
+    const remLowerText = remText.toLowerCase();
+    startIndex = remLowerText.indexOf(searchTerm);
+    if (startIndex >= 0) {
+      highLightedText += `${remText.substring(0, startIndex)}<strong>${remText.substring(
+        startIndex,
+        startIndex + searchTerm.length
+      )}</strong>`;
+      remText = remText.substring(startIndex + searchTerm.length);
+    } else {
+      highLightedText += remText;
+    }
+  } while (startIndex >= 0);
+
+  return highLightedText;
+};
+
+export default highlight;

+ 23 - 0
desktop/core/src/desktop/js/utils/html/html2text.ts

@@ -0,0 +1,23 @@
+// 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.
+
+const html2text = (value: string): string => {
+  const element = document.createElement('div');
+  element.innerHTML = value;
+  return element.innerText.replace(/\u00A0/g, ' ');
+};
+
+export default html2text;

+ 27 - 0
desktop/core/src/desktop/js/utils/html/htmlEncode.ts

@@ -0,0 +1,27 @@
+// 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.
+
+/**
+ * Create a in-memory div, set it's inner text(which jQuery automatically encodes)
+ * then grab the encoded contents back out.
+ */
+const htmlEncode = (value: string): string => {
+  const element = document.createElement('div');
+  element.innerText = value;
+  return element.innerHTML;
+};
+
+export default htmlEncode;

+ 25 - 0
desktop/core/src/desktop/js/utils/html/isOverflowing.ts

@@ -0,0 +1,25 @@
+// 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.
+
+const isOverflowing = (element: HTMLElement | JQuery): boolean => {
+  const htmlElement = (<JQuery>element).jquery ? (<JQuery>element).get(0) : <HTMLElement>element;
+  return (
+    htmlElement.scrollHeight > htmlElement.clientHeight ||
+    htmlElement.scrollWidth > htmlElement.clientWidth
+  );
+};
+
+export default isOverflowing;

+ 39 - 0
desktop/core/src/desktop/js/utils/html/onHueLinkClick.ts

@@ -0,0 +1,39 @@
+// 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 { hueWindow } from 'types/types';
+import huePubSub from 'utils/huePubSub';
+
+const onHueLinkClick = (event: Event, url: string, target?: string): void => {
+  if (url.indexOf('http') === 0) {
+    window.open(url, target);
+  } else {
+    const prefix = (<hueWindow>window).HUE_BASE_URL + '/hue' + (url.indexOf('/') === 0 ? '' : '/');
+    if (target) {
+      window.open(prefix + url, target);
+    } else if (
+      (<KeyboardEvent>event).ctrlKey ||
+      (<KeyboardEvent>event).metaKey ||
+      (<KeyboardEvent>event).which === 2
+    ) {
+      window.open(prefix + url, '_blank');
+    } else {
+      huePubSub.publish('open.link', url);
+    }
+  }
+};
+
+export default onHueLinkClick;

+ 40 - 0
desktop/core/src/desktop/js/utils/html/stripHtmlForFunctions.ts

@@ -0,0 +1,40 @@
+// 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.
+
+const stripHtml = (html: string): string => {
+  const tmp = document.createElement('DIV');
+  tmp.innerHTML = html;
+  return tmp.textContent || tmp.innerText;
+};
+
+const stripHtmlFromFunctions = (template: string): string => {
+  // strips HTML from inside the functions
+  let stripped = template;
+  const mustacheFunctions = stripped.match(/{{#(.[\s\S]*?){{\//g);
+  if (mustacheFunctions) {
+    mustacheFunctions.forEach(fn => {
+      stripped = stripped.replace(
+        fn,
+        fn.substr(0, fn.indexOf('}}') + 2) +
+          stripHtml(fn.substr(fn.indexOf('}}') + 2).slice(0, -3)).trim() +
+          '{{/'
+      );
+    });
+  }
+  return stripped;
+};
+
+export default stripHtmlFromFunctions;

+ 31 - 0
desktop/core/src/desktop/js/utils/hueAnalytics.ts

@@ -0,0 +1,31 @@
+// 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 { post } from '../api/utils';
+import { hueWindow } from '../types/types';
+
+export default {
+  log(app: string, page: string): void {
+    if (typeof (<hueWindow>window).trackOnGA == 'function') {
+      (<hueWindow>window).trackOnGA!(app + '/' + page);
+    }
+  },
+  convert(app: string, page: string): void {
+    post('/desktop/log_analytics', {
+      page: app + '/' + page
+    });
+  }
+};

+ 0 - 92
desktop/core/src/desktop/js/utils/hueUtils.test.ts

@@ -1,92 +0,0 @@
-// Licensed to Cloudera, Inc. under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  Cloudera, Inc. licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import hueUtils from './hueUtils';
-
-describe('hue.utils.js', () => {
-  it('should change completely the URL', () => {
-    hueUtils.changeURL('/banana');
-
-    expect(window.location.pathname).toEqual('/banana');
-    hueUtils.changeURL('/jasmine');
-  });
-
-  it('should change just a parameter in the URL', () => {
-    hueUtils.changeURL('/banana?peeled=no');
-    hueUtils.changeURLParameter('peeled', 'yes');
-
-    expect(window.location.search).toEqual('?peeled=yes');
-    hueUtils.changeURL('/jasmine');
-  });
-
-  it('should remove a parameter in the URL', () => {
-    hueUtils.changeURL('/banana?peeled=no');
-    hueUtils.removeURLParameter('peeled');
-
-    expect(window.location.search).toEqual('');
-    hueUtils.changeURL('/jasmine');
-  });
-
-  it('should return empty string when the value is undefined', () => {
-    expect(hueUtils.deXSS(undefined)).toEqual('');
-  });
-
-  it('should return false when the value is false', () => {
-    expect(hueUtils.deXSS(false)).toEqual('false');
-  });
-
-  it('should return 0 when the value is 0', () => {
-    expect(hueUtils.deXSS(0)).toEqual('0');
-  });
-
-  it('should return null when the value is null', () => {
-    expect(hueUtils.deXSS(null)).toEqual('null');
-  });
-
-  it('should remove JS code from a string', () => {
-    expect(hueUtils.deXSS('hello <script>alert(123)</script>world')).toEqual('hello world');
-  });
-
-  it('Should not remove strong tag from a string', () => {
-    expect(hueUtils.deXSS('<strong>Hello World</strong>')).toEqual('<strong>Hello World</strong>');
-  });
-
-  it('Should remove img tag from a string', () => {
-    expect(hueUtils.deXSS('<img src=asf onerror=alert(document.cookie) />hello')).toEqual('hello');
-  });
-
-  it('Should not remove p tag from a string', () => {
-    expect(hueUtils.deXSS('<p> Hello World </p>')).toEqual('<p> Hello World </p>');
-  });
-
-  it('Should not remove h1 tag from a string', () => {
-    expect(hueUtils.deXSS('<h1> Hello World </h1>')).toEqual('<h1> Hello World </h1>');
-  });
-
-  it('should preserve entities as such', () => {
-    expect(hueUtils.deXSS('<a name="&lt;silly&gt;">&lt;Kapow!&gt;</a>')).toEqual(
-      '<a name="&lt;silly&gt;">&lt;Kapow!&gt;</a>'
-    );
-  });
-
-  it('should dump comments', () => {
-    expect(hueUtils.deXSS('<p><!-- Blah blah -->Whee</p>')).toEqual('<p>Whee</p>');
-  });
-
-  it('should dump an uppercase javascript url', () => {
-    expect(hueUtils.deXSS('<a href="JAVASCRIPT:alert(\'foo\')">Hax</a>')).toEqual('<a>Hax</a>');
-  });
-});

+ 34 - 591
desktop/core/src/desktop/js/utils/hueUtils.ts

@@ -14,598 +14,42 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import $ from 'jquery';
-import sanitizeHtml from 'sanitize-html';
-import { hueWindow } from 'types/types';
-import huePubSub from 'utils/huePubSub';
 import { hueLocalStorage, withLocalStorage } from './storageUtils';
 
-export const bootstrapRatios = {
-  span3(): number {
-    if (window.innerWidth >= 1200) {
-      return 23.07692308;
-    } else if (window.innerWidth >= 768 && window.innerWidth <= 979) {
-      return 22.9281768;
-    } else {
-      return 23.17073171;
-    }
-  },
-  span9(): number {
-    if (window.innerWidth >= 1200) {
-      return 74.35897436;
-    } else if (window.innerWidth >= 768 && window.innerWidth <= 979) {
-      return 74.30939227;
-    } else {
-      return 74.3902439;
-    }
-  },
-  margin(): number {
-    return 2.56410256;
-  }
-};
-
-/**
- * Create a in-memory div, set it's inner text(which jQuery automatically encodes)
- * then grab the encoded contents back out.
- */
-export const htmlEncode = (value: string): string => {
-  const element = document.createElement('div');
-  element.innerText = value;
-  return element.innerHTML;
-};
-
-export const html2text = (value: string): string => {
-  const element = document.createElement('div');
-  element.innerHTML = value;
-  return element.innerText.replace(/\u00A0/g, ' ');
-};
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-type PlainObject = { [name: string]: any };
-
-export const isFullScreen = (): boolean => {
-  return (
-    document.fullscreenElement ||
-    (<PlainObject>document).mozFullScreenElement ||
-    (<PlainObject>document).webkitFullscreenElement ||
-    (<PlainObject>document).msFullscreenElement
-  );
-};
-
-export const goFullScreen = async (element: Element): Promise<void> => {
-  if (isFullScreen()) {
-    return;
-  }
-  if (!element) {
-    element = document.documentElement;
-  }
-
-  if (element.requestFullscreen) {
-    return element.requestFullscreen();
-  }
-  const mixedBrowserElement = element as PlainObject;
-  if (mixedBrowserElement.msRequestFullscreen) {
-    return mixedBrowserElement.msRequestFullscreen();
-  }
-  if (mixedBrowserElement.mozRequestFullScreen) {
-    return mixedBrowserElement.mozRequestFullScreen();
-  }
-  if (mixedBrowserElement.webkitRequestFullscreen) {
-    return mixedBrowserElement.webkitRequestFullscreen(mixedBrowserElement.ALLOW_KEYBOARD_INPUT);
-  }
-};
-
-export const exitFullScreen = async (): Promise<void> => {
-  if (!isFullScreen()) {
-    return;
-  }
-  if (document.exitFullscreen) {
-    return document.exitFullscreen();
-  }
-  const mixedBrowserElement = document as PlainObject;
-  if (mixedBrowserElement.msExitFullscreen) {
-    return mixedBrowserElement.msExitFullscreen();
-  }
-  if (mixedBrowserElement.mozCancelFullScreen) {
-    return mixedBrowserElement.mozCancelFullScreen();
-  }
-  if (mixedBrowserElement.webkitExitFullscreen) {
-    return mixedBrowserElement.webkitExitFullscreen();
-  }
-};
-
-export const toggleFullScreen = async (element: Element): Promise<void> => {
-  if (isFullScreen()) {
-    return exitFullScreen();
-  }
-  return goFullScreen(element);
-};
-
-export const changeURL = (newURL: string, params?: PlainObject): void => {
-  let extraSearch = '';
-  if (params) {
-    const newSearchKeys = Object.keys(params);
-    if (newSearchKeys.length) {
-      while (newSearchKeys.length) {
-        const newKey = newSearchKeys.pop() || '';
-        extraSearch += newKey + '=' + params[newKey];
-        if (newSearchKeys.length) {
-          extraSearch += '&';
-        }
-      }
-    }
-  }
-
-  const hashSplit = newURL.split('#');
-  const hueBaseUrl = (<hueWindow>window).HUE_BASE_URL;
-  const base =
-    hueBaseUrl && hashSplit[0].length && hashSplit[0].indexOf(hueBaseUrl) !== 0 ? hueBaseUrl : '';
-  let url = base + hashSplit[0];
-  if (extraSearch) {
-    url += (url.indexOf('?') === -1 ? '?' : '&') + extraSearch;
-  }
-  if (hashSplit.length > 1) {
-    //the foldername may contain # , so create substring ignoring first #
-    url += '#' + newURL.substring(newURL.indexOf('#') + 1);
-  } else if (window.location.hash) {
-    url += window.location.hash;
-  }
-  window.history.pushState(null, '', url);
-};
-
-export const replaceURL = (newURL: string): void => {
-  window.history.replaceState(null, '', newURL);
-};
-
-export const changeURLParameter = (param: string, value: string | null): void => {
-  let newSearch = '';
-  if (getParameter(param, true) !== null) {
-    newSearch += '?';
-    window.location.search
-      .replace(/\?/gi, '')
-      .split('&')
-      .forEach(p => {
-        if (p.split('=')[0] !== param) {
-          newSearch += p;
-        }
-      });
-    if (value) {
-      newSearch += (newSearch !== '?' ? '&' : '') + param + '=' + value;
-    }
-  } else {
-    newSearch =
-      window.location.search +
-      (value ? (window.location.search.indexOf('?') > -1 ? '&' : '?') + param + '=' + value : '');
-  }
-
-  if (newSearch === '?') {
-    newSearch = '';
-  }
-
-  changeURL(window.location.pathname + newSearch);
-};
-
-export const removeURLParameter = (param: string): void => {
-  changeURLParameter(param, null);
-};
-
-export const parseHivePseudoJson = (pseudoJson: string): { [key: string]: string } => {
-  // Hive returns a pseudo-json with parameters, like
-  // "{Lead Developer=John Foo, Lead Developer Email=jfoo@somewhere.com, date=2013-07-11 }"
-  const parsedParams: { [key: string]: string } = {};
-  if (pseudoJson && pseudoJson.length > 2) {
-    const splits = pseudoJson.substring(1, pseudoJson.length - 1).split(', ');
-    splits.forEach(part => {
-      if (part.indexOf('=') > -1) {
-        parsedParams[part.split('=')[0]] = part.split('=')[1];
-      }
-    });
-  }
-  return parsedParams;
-};
-
-export const isOverflowing = (element: HTMLElement | JQuery): boolean => {
-  const htmlElement = (<JQuery>element).jquery ? (<JQuery>element).get(0) : <HTMLElement>element;
-  return (
-    htmlElement.scrollHeight > htmlElement.clientHeight ||
-    htmlElement.scrollWidth > htmlElement.clientWidth
-  );
-};
-
-export const waitForRendered = (
-  selector: string | JQuery,
-  condition: (element: JQuery) => boolean,
-  callback: (element: JQuery) => void,
-  timeout?: number
-): void => {
-  const $el = (<JQuery>selector).jquery ? <JQuery>selector : $(<string>selector);
-  if (condition($el)) {
-    callback($el);
-  } else {
-    window.clearTimeout($el.data('waitForRenderTimeout'));
-    const waitForRenderTimeout = window.setTimeout(() => {
-      waitForRendered(selector, condition, callback);
-    }, timeout || 100);
-    $el.data('waitForRenderTimeout', waitForRenderTimeout);
-  }
-};
-
-export const waitForObservable = (
-  observable: KnockoutObservable<unknown>,
-  callback: (observable: KnockoutObservable<unknown>) => void
-): void => {
-  if (observable()) {
-    callback(observable);
-  } else {
-    const subscription = observable.subscribe(newValue => {
-      if (newValue) {
-        subscription.dispose();
-        callback(observable);
-      }
-    });
-  }
-};
-
-export const waitForVariable = (
-  variable: unknown,
-  callback: (variable: unknown) => void,
-  timeout?: number
-): void => {
-  if (variable) {
-    callback(variable);
-  } else {
-    window.setTimeout(() => {
-      waitForVariable(variable, callback);
-    }, timeout || 100);
-  }
-};
-
-export const scrollbarWidth = (): number => {
-  const parent = document.createElement('div');
-  parent.style.width = '50px';
-  parent.style.height = '50px';
-  parent.style.overflow = 'auto';
-  const child = document.createElement('div');
-  child.style.height = '100px';
-  parent.append(child);
-  document.body.append(child);
-  const width = child.offsetWidth - child.clientWidth;
-  parent.remove();
-  return width;
-};
-
-export const getParameter = (name: string, nullAllowed?: boolean): string | null => {
-  return getSearchParameter(window.location.search, name, nullAllowed);
-};
-
-export const getSearchParameter = (
-  search: string,
-  name: string,
-  nullAllowed?: boolean
-): string | null => {
-  name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
-  const regex = new RegExp('[\\?&]' + name + '=([^&#]*)'),
-    results = regex.exec(search);
-  if (nullAllowed && results === null) {
-    return null;
-  }
-  return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
-};
-
-export const logError = (error: unknown): void => {
-  if (typeof window.console !== 'undefined' && typeof window.console.error !== 'undefined') {
-    if (typeof error !== 'undefined') {
-      console.error(error);
-    }
-    console.error(new Error().stack);
-  }
-};
-
-export const equalIgnoreCase = (a?: string, b?: string): boolean =>
-  !!a && !!b && a.toLowerCase() === b.toLowerCase();
-
-export const deXSS = (str?: string | number | null): string =>
-  (typeof str !== 'undefined' && sanitizeHtml(str as string)) || '';
-
-export const getStyleFromCSSClass = (
-  cssClass: string
-): CSSStyleDeclaration | CSSRule | undefined => {
-  for (let i = 0; i < document.styleSheets.length; i++) {
-    const cssClasses = document.styleSheets[i].rules || document.styleSheets[i].cssRules;
-    for (let x = 0; x < cssClasses.length; x++) {
-      if ((<CSSStyleRule>cssClasses[x]).selectorText === cssClass) {
-        return (<CSSStyleRule>cssClasses[x]).style
-          ? (<CSSStyleRule>cssClasses[x]).style
-          : <CSSStyleRule>cssClasses[x];
-      }
-    }
-  }
-};
-
-export const highlight = (text: string, searchTerm: string): string => {
-  if (searchTerm === '' || text === '') {
-    return text;
-  }
-
-  let remText = text;
-  let highLightedText = '';
-  searchTerm = searchTerm.toLowerCase();
-
-  let startIndex;
-  do {
-    const remLowerText = remText.toLowerCase();
-    startIndex = remLowerText.indexOf(searchTerm);
-    if (startIndex >= 0) {
-      highLightedText += `${remText.substring(0, startIndex)}<strong>${remText.substring(
-        startIndex,
-        startIndex + searchTerm.length
-      )}</strong>`;
-      remText = remText.substring(startIndex + searchTerm.length);
-    } else {
-      highLightedText += remText;
-    }
-  } while (startIndex >= 0);
-
-  return highLightedText;
-};
-
-type Node = { [key: string]: Node };
-
-export const dfs = (node: Node, callback: (node: Node, key: string) => void): void => {
-  if (!node || typeof node !== 'object') {
-    return;
-  }
-  Object.keys(node).forEach(key => {
-    callback(node, key);
-    dfs(node[key], callback);
-  });
-};
-
-export const deleteAllEmptyStringKeys = (node: Node): void => {
-  dfs(node, (node: Node, key: string) => {
-    if (node[key] || typeof node[key] !== 'string') {
-      return;
-    }
-    delete node[key];
-  });
-};
-
-const s4 = (): string =>
-  Math.floor((1 + Math.random()) * 0x10000)
-    .toString(16)
-    .substring(1);
-
-export const UUID = (): string =>
-  s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
-
-export const escapeOutput = (str: string): string => htmlEncode(str).trim();
-
-type ComplexValueAccessor = () => {
-  value?: KnockoutObservable<string>;
-  displayJustLastBit?: boolean;
-};
-
-export const getFileBrowseButton = (
-  inputElement: JQuery<HTMLInputElement>,
-  selectFolder: string,
-  valueAccessor: KnockoutObservable<string> | ComplexValueAccessor,
-  stripHdfsPrefix: boolean,
-  allBindingsAccessor: KnockoutAllBindingsAccessor,
-  isAddon: boolean,
-  isNestedModal: boolean,
-  linkMarkup: boolean
-): JQuery => {
-  let button: JQuery;
-  if (isAddon) {
-    button = $('<span>').addClass('add-on muted pointer filechooser-clickable').text('..');
-  } else if (linkMarkup) {
-    button = $('<a>').addClass('btn').addClass('fileChooserBtn filechooser-clickable').text('..');
-  } else {
-    button = $('<button>')
-      .addClass('btn')
-      .addClass('fileChooserBtn filechooser-clickable')
-      .text('..');
-  }
-  button.on('click', e => {
-    e.preventDefault();
-    if (!isNestedModal) {
-      $('body').addClass('modal-open');
-    }
-
-    function callFileChooser() {
-      let initialPath: string =
-        (<string>inputElement.val()).trim() !== '' ? <string>inputElement.val() || '' : '/';
-      if (
-        (allBindingsAccessor &&
-          allBindingsAccessor().filechooserOptions &&
-          allBindingsAccessor().filechooserOptions.skipInitialPathIfEmpty &&
-          inputElement.val() === '') ||
-        (allBindingsAccessor && allBindingsAccessor().filechooserPrefixSeparator)
-      ) {
-        initialPath = '';
-      }
-      if (inputElement.data('fullPath')) {
-        initialPath = inputElement.data('fullPath');
-      }
-      if (initialPath.indexOf('hdfs://') > -1) {
-        initialPath = initialPath.substring(7);
-      }
-
-      let supportSelectFolder = !!selectFolder;
-      if (
-        allBindingsAccessor &&
-        typeof allBindingsAccessor().filechooserOptions !== 'undefined' &&
-        typeof allBindingsAccessor().filechooserOptions.selectFolder !== 'undefined'
-      ) {
-        supportSelectFolder = allBindingsAccessor().filechooserOptions.selectFolder;
-      }
-
-      $('#filechooser').jHueFileChooser({
-        suppressErrors: true,
-        selectFolder: supportSelectFolder,
-        onFolderChoose: filePath => {
-          handleChoice(filePath, stripHdfsPrefix);
-          if (selectFolder) {
-            $('#chooseFile').modal('hide');
-            if (!isNestedModal) {
-              $('.modal-backdrop').remove();
-            }
-          }
-        },
-        onFileChoose: filePath => {
-          handleChoice(filePath, stripHdfsPrefix);
-          $('#chooseFile').modal('hide');
-          if (!isNestedModal) {
-            $('.modal-backdrop').remove();
-          }
-        },
-        createFolder:
-          allBindingsAccessor &&
-          allBindingsAccessor().filechooserOptions &&
-          allBindingsAccessor().filechooserOptions.createFolder,
-        uploadFile:
-          allBindingsAccessor &&
-          allBindingsAccessor().filechooserOptions &&
-          allBindingsAccessor().filechooserOptions.uploadFile,
-        initialPath: initialPath,
-        errorRedirectPath: '',
-        forceRefresh: true,
-        showExtraHome:
-          allBindingsAccessor &&
-          allBindingsAccessor().filechooserOptions &&
-          allBindingsAccessor().filechooserOptions.showExtraHome,
-        extraHomeProperties:
-          allBindingsAccessor &&
-          allBindingsAccessor().filechooserOptions &&
-          allBindingsAccessor().filechooserOptions.extraHomeProperties
-            ? allBindingsAccessor().filechooserOptions.extraHomeProperties
-            : {},
-        filterExtensions:
-          allBindingsAccessor && allBindingsAccessor().filechooserFilter
-            ? allBindingsAccessor().filechooserFilter
-            : '',
-        displayOnlyFolders:
-          allBindingsAccessor &&
-          allBindingsAccessor().filechooserOptions &&
-          allBindingsAccessor().filechooserOptions.displayOnlyFolders
-      });
-      $('#chooseFile').modal('show');
-      if (!isNestedModal) {
-        $('#chooseFile').on('hidden', () => {
-          $('body').removeClass('modal-open');
-          $('.modal-backdrop').remove();
-        });
-      }
-    }
-
-    // check if it's a relative path
-    callFileChooser();
-
-    const handleChoice = (filePath: string, stripHdfsPrefix: boolean) => {
-      if (allBindingsAccessor && allBindingsAccessor().filechooserPrefixSeparator) {
-        filePath =
-          (<string>inputElement.val()).split(allBindingsAccessor().filechooserPrefixSeparator)[0] +
-          '=' +
-          filePath;
-      }
-      if (
-        allBindingsAccessor &&
-        allBindingsAccessor().filechooserOptions &&
-        allBindingsAccessor().filechooserOptions.deploymentDir
-      ) {
-        inputElement.data('fullPath', filePath);
-        inputElement.attr('data-original-title', filePath);
-        if (filePath.indexOf(allBindingsAccessor().filechooserOptions.deploymentDir) === 0) {
-          filePath = filePath.substr(
-            allBindingsAccessor().filechooserOptions.deploymentDir.length + 1
-          );
-        }
-      }
-      if (stripHdfsPrefix) {
-        inputElement.val(filePath);
-      } else {
-        inputElement.val('hdfs://' + filePath);
-      }
-      inputElement.trigger('change');
-      if (valueAccessor) {
-        if (
-          typeof valueAccessor() === 'function' ||
-          typeof (<ComplexValueAccessor>valueAccessor)().value === 'function'
-        ) {
-          const complex = (<ComplexValueAccessor>valueAccessor)();
-          if (complex.value) {
-            complex.value(<string>inputElement.val());
-            if (complex.displayJustLastBit) {
-              inputElement.data('fullPath', <string>inputElement.val());
-              inputElement.attr('data-original-title', <string>inputElement.val());
-              const value = <string>inputElement.val();
-              inputElement.val(value.split('/')[value.split('/').length - 1]);
-            }
-            return;
-          }
-        }
-        (<KnockoutObservable<string>>valueAccessor())(<string>inputElement.val());
-      }
-    };
-  });
-  if (allBindingsAccessor && allBindingsAccessor().filechooserDisabled) {
-    button.addClass('disabled').attr('disabled', 'disabled');
-  }
-  return button;
-};
-
-const stripHtml = (html: string): string => {
-  const tmp = document.createElement('DIV');
-  tmp.innerHTML = html;
-  return tmp.textContent || tmp.innerText;
-};
-
-export const stripHtmlFromFunctions = (template: string): string => {
-  // strips HTML from inside the functions
-  let stripped = template;
-  const mustacheFunctions = stripped.match(/{{#(.[\s\S]*?){{\//g);
-  if (mustacheFunctions) {
-    mustacheFunctions.forEach(fn => {
-      stripped = stripped.replace(
-        fn,
-        fn.substr(0, fn.indexOf('}}') + 2) +
-          stripHtml(fn.substr(fn.indexOf('}}') + 2).slice(0, -3)).trim() +
-          '{{/'
-      );
-    });
-  }
-  return stripped;
-};
-
-export const onHueLinkClick = (event: Event, url: string, target?: string): void => {
-  if (url.indexOf('http') === 0) {
-    window.open(url, target);
-  } else {
-    const prefix = (<hueWindow>window).HUE_BASE_URL + '/hue' + (url.indexOf('/') === 0 ? '' : '/');
-    if (target) {
-      window.open(prefix + url, target);
-    } else if (
-      (<KeyboardEvent>event).ctrlKey ||
-      (<KeyboardEvent>event).metaKey ||
-      (<KeyboardEvent>event).which === 2
-    ) {
-      window.open(prefix + url, '_blank');
-    } else {
-      huePubSub.publish('open.link', url);
-    }
-  }
-};
-
-export const sleep = async (timeout: number): Promise<void> =>
-  new Promise(resolve => setTimeout(resolve, timeout));
-
-export const defer = (callback: () => void): void => {
-  sleep(0).finally(callback);
-};
-
-export const noop = (): void => {
-  // noop
-};
+import logError from './logError';
+
+import exitFullScreen from './screen/exitFullsScreen';
+import goFullScreen from './screen/goFullScreen';
+import isFullScreen from './screen/isFullScreen';
+import scrollbarWidth from './screen/scrollbarWidth';
+import toggleFullScreen from './screen/toggleFullScreen';
+
+import bootstrapRatios from './html/bootstrapRatios';
+import deXSS from './html/deXSS';
+import escapeOutput from './html/escapeOutput';
+import getFileBrowseButton from './html/getFileBrowseButton';
+import getStyleFromCSSClass from './html/getStyleFromCSSClass';
+import highlight from './html/highlight';
+import html2text from './html/html2text';
+import htmlEncode from './html/htmlEncode';
+import isOverflowing from './html/isOverflowing';
+import stripHtmlFromFunctions from './html/stripHtmlForFunctions';
+
+import deleteAllEmptyStringKeys from './string/deleteAllEmptyStringKeys';
+import equalIgnoreCase from './string/equalIgnoreCase';
+import parseHivePseudoJson from './string/parseHivePseudoJson';
+import UUID from './string/UUID';
+
+import waitForObservable from './timing/waitForObservable';
+import waitForRendered from './timing/waitForRendered';
+import waitForVariable from './timing/waitForVariable';
+
+import changeURL from './url/changeURL';
+import changeURLParameter from './url/changeURLParameter';
+import getParameter from './url/getParameter';
+import getSearchParameter from './url/getSearchParameter';
+import removeURLParameter from './url/removeURLParameter';
+import replaceURL from './url/replaceURL';
 
 export default {
   bootstrapRatios,
@@ -613,7 +57,6 @@ export default {
   changeURLParameter,
   deleteAllEmptyStringKeys,
   deXSS,
-  dfs,
   equalIgnoreCase,
   escapeOutput,
   exitFullScreen,

+ 6 - 12
desktop/core/src/desktop/js/utils/hueAnalytics.js → desktop/core/src/desktop/js/utils/logError.ts

@@ -14,19 +14,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import $ from 'jquery';
-
-const hueAnalytics = {
-  log: function (app, page) {
-    if (typeof trackOnGA == 'function') {
-      trackOnGA(app + '/' + page);
+const logError = (error: unknown): void => {
+  if (typeof window.console !== 'undefined' && typeof window.console.error !== 'undefined') {
+    if (typeof error !== 'undefined') {
+      console.error(error);
     }
-  },
-  convert: function (app, page) {
-    $.post('/desktop/log_analytics', {
-      page: app + '/' + page
-    });
+    console.error(new Error().stack);
   }
 };
 
-export default hueAnalytics;
+export default logError;

+ 4 - 4
desktop/core/src/desktop/js/utils/multiLineEllipsisHandler.js

@@ -17,7 +17,7 @@
 import $ from 'jquery';
 import * as ko from 'knockout';
 
-import hueUtils from 'utils/hueUtils';
+import deXSS from 'utils/html/deXSS';
 
 const checkOverflow = element =>
   element &&
@@ -43,13 +43,13 @@ class MultiLineEllipsisHandler {
     this.updateOverflowHeight();
 
     this.contents = options.text;
-    this.element.innerHTML = hueUtils.deXSS(this.contents);
+    this.element.innerHTML = deXSS(this.contents);
 
     const linkRegex = /(?:(?:[a-z]+:\/\/)|www\.)[^\s\/]+(?:[.\/]\S+)*[^\s`!()\[\]{};:'".,<>?«»“”‘’]/gi;
 
     this.renderContents = contents => {
       if (options.linkify) {
-        return hueUtils.deXSS(
+        return deXSS(
           contents.replace(linkRegex, val => {
             return (
               '<a href="' +
@@ -61,7 +61,7 @@ class MultiLineEllipsisHandler {
           })
         );
       }
-      return hueUtils.deXSS(contents);
+      return deXSS(contents);
     };
 
     this.delayedResumeTimeout = window.setTimeout(() => {

+ 44 - 0
desktop/core/src/desktop/js/utils/screen/exitFullsScreen.ts

@@ -0,0 +1,44 @@
+// 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 isFullScreen from './isFullScreen';
+
+interface FullScreenDocument extends Document {
+  msExitFullscreen?: Document['exitFullscreen'];
+  mozCancelFullScreen?: Document['exitFullscreen'];
+  webkitExitFullscreen?: Document['exitFullscreen'];
+}
+
+const exitFullScreen = async (): Promise<void> => {
+  if (!isFullScreen()) {
+    return;
+  }
+  if (document.exitFullscreen) {
+    return document.exitFullscreen();
+  }
+  const mixedBrowserElement = document as FullScreenDocument;
+  if (mixedBrowserElement.msExitFullscreen) {
+    return mixedBrowserElement.msExitFullscreen();
+  }
+  if (mixedBrowserElement.mozCancelFullScreen) {
+    return mixedBrowserElement.mozCancelFullScreen();
+  }
+  if (mixedBrowserElement.webkitExitFullscreen) {
+    return mixedBrowserElement.webkitExitFullscreen();
+  }
+};
+
+export default exitFullScreen;

+ 49 - 0
desktop/core/src/desktop/js/utils/screen/goFullScreen.ts

@@ -0,0 +1,49 @@
+// 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 isFullScreen from './isFullScreen';
+
+interface FullscreenElement extends HTMLElement {
+  msRequestFullscreen?: HTMLElement['requestFullscreen'];
+  mozRequestFullScreen?: HTMLElement['requestFullscreen'];
+  webkitRequestFullscreen?(allowKeyboardInput?: boolean): Promise<void>;
+  ALLOW_KEYBOARD_INPUT?: boolean;
+}
+
+const goFullScreen = async (element: Element): Promise<void> => {
+  if (isFullScreen()) {
+    return;
+  }
+  if (!element) {
+    element = document.documentElement;
+  }
+
+  if (element.requestFullscreen) {
+    return element.requestFullscreen();
+  }
+  const mixedBrowserElement = element as FullscreenElement;
+  if (mixedBrowserElement.msRequestFullscreen) {
+    return mixedBrowserElement.msRequestFullscreen();
+  }
+  if (mixedBrowserElement.mozRequestFullScreen) {
+    return mixedBrowserElement.mozRequestFullScreen();
+  }
+  if (mixedBrowserElement.webkitRequestFullscreen) {
+    return mixedBrowserElement.webkitRequestFullscreen(mixedBrowserElement.ALLOW_KEYBOARD_INPUT);
+  }
+};
+
+export default goFullScreen;

+ 32 - 0
desktop/core/src/desktop/js/utils/screen/isFullScreen.ts

@@ -0,0 +1,32 @@
+// 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.
+
+interface FullScreenDocument extends Document {
+  mozFullScreenElement?: Element | null;
+  webkitFullscreenElement?: Element | null;
+  msFullscreenElement?: Element | null;
+}
+
+const isFullScreen = (): boolean => {
+  return !!(
+    document.fullscreenElement ||
+    (<FullScreenDocument>document).mozFullScreenElement ||
+    (<FullScreenDocument>document).webkitFullscreenElement ||
+    (<FullScreenDocument>document).msFullscreenElement
+  );
+};
+
+export default isFullScreen;

+ 31 - 0
desktop/core/src/desktop/js/utils/screen/scrollbarWidth.ts

@@ -0,0 +1,31 @@
+// 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.
+
+const scrollbarWidth = (): number => {
+  const parent = document.createElement('div');
+  parent.style.width = '50px';
+  parent.style.height = '50px';
+  parent.style.overflow = 'auto';
+  const child = document.createElement('div');
+  child.style.height = '100px';
+  parent.append(child);
+  document.body.append(child);
+  const width = child.offsetWidth - child.clientWidth;
+  parent.remove();
+  return width;
+};
+
+export default scrollbarWidth;

+ 28 - 0
desktop/core/src/desktop/js/utils/screen/toggleFullScreen.ts

@@ -0,0 +1,28 @@
+// 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 exitFullScreen from './exitFullsScreen';
+import goFullScreen from './goFullScreen';
+import isFullScreen from './isFullScreen';
+
+const toggleFullScreen = async (element: Element): Promise<void> => {
+  if (isFullScreen()) {
+    return exitFullScreen();
+  }
+  return goFullScreen(element);
+};
+
+export default toggleFullScreen;

+ 25 - 0
desktop/core/src/desktop/js/utils/string/UUID.ts

@@ -0,0 +1,25 @@
+// 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.
+
+const s4 = (): string =>
+  Math.floor((1 + Math.random()) * 0x10000)
+    .toString(16)
+    .substring(1);
+
+const UUID = (): string =>
+  s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
+
+export default UUID;

+ 38 - 0
desktop/core/src/desktop/js/utils/string/deleteAllEmptyStringKeys.ts

@@ -0,0 +1,38 @@
+// 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.
+
+export type Node = { [key: string]: Node };
+
+const dfs = (node: Node, callback: (node: Node, key: string) => void): void => {
+  if (!node || typeof node !== 'object') {
+    return;
+  }
+  Object.keys(node).forEach(key => {
+    callback(node, key);
+    dfs(node[key], callback);
+  });
+};
+
+const deleteAllEmptyStringKeys = (node: Node): void => {
+  dfs(node, (node: Node, key: string) => {
+    if (node[key] || typeof node[key] !== 'string') {
+      return;
+    }
+    delete node[key];
+  });
+};
+
+export default deleteAllEmptyStringKeys;

+ 20 - 0
desktop/core/src/desktop/js/utils/string/equalIgnoreCase.ts

@@ -0,0 +1,20 @@
+// 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.
+
+const equalIgnoreCase = (a?: string, b?: string): boolean =>
+  !!a && !!b && a.toLowerCase() === b.toLowerCase();
+
+export default equalIgnoreCase;

+ 32 - 0
desktop/core/src/desktop/js/utils/string/parseHivePseudoJson.ts

@@ -0,0 +1,32 @@
+// 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.
+
+const parseHivePseudoJson = (pseudoJson: string): { [key: string]: string } => {
+  // Hive returns a pseudo-json with parameters, like
+  // "{Lead Developer=John Foo, Lead Developer Email=jfoo@somewhere.com, date=2013-07-11 }"
+  const parsedParams: { [key: string]: string } = {};
+  if (pseudoJson && pseudoJson.length > 2) {
+    const splits = pseudoJson.substring(1, pseudoJson.length - 1).split(', ');
+    splits.forEach(part => {
+      if (part.indexOf('=') > -1) {
+        parsedParams[part.split('=')[0]] = part.split('=')[1];
+      }
+    });
+  }
+  return parsedParams;
+};
+
+export default parseHivePseudoJson;

+ 23 - 0
desktop/core/src/desktop/js/utils/timing/defer.ts

@@ -0,0 +1,23 @@
+// 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 sleep from './sleep';
+
+const defer = (callback: () => void): void => {
+  sleep(0).finally(callback);
+};
+
+export default defer;

+ 21 - 0
desktop/core/src/desktop/js/utils/timing/noop.ts

@@ -0,0 +1,21 @@
+// 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.
+
+const noop = (): void => {
+  // noop
+};
+
+export default noop;

Some files were not shown because too many files changed in this diff