瀏覽代碼

HUE-9090 [frontend] Add additional ko component tests

Johan Ahlen 6 年之前
父節點
當前提交
349976dcb8
共有 34 個文件被更改,包括 800 次插入156 次删除
  1. 17 1
      desktop/core/src/desktop/js/api/apiHelper.js
  2. 7 2
      desktop/core/src/desktop/js/jest/jest.init.js
  3. 19 11
      desktop/core/src/desktop/js/jest/koTestUtils.js
  4. 119 87
      desktop/core/src/desktop/js/ko/components/__snapshots__/ko.catalogEntriesList.test.js.snap
  5. 10 0
      desktop/core/src/desktop/js/ko/components/__snapshots__/ko.contextSelector.test.js.snap
  6. 16 0
      desktop/core/src/desktop/js/ko/components/__snapshots__/ko.dropDown.test.js.snap
  7. 14 0
      desktop/core/src/desktop/js/ko/components/__snapshots__/ko.executionAnalysis.test.js.snap
  8. 13 0
      desktop/core/src/desktop/js/ko/components/__snapshots__/ko.favoriteApp.test.js.snap
  9. 99 0
      desktop/core/src/desktop/js/ko/components/__snapshots__/ko.fieldSamples.test.js.snap
  10. 29 0
      desktop/core/src/desktop/js/ko/components/__snapshots__/ko.globalSearch.test.js.snap
  11. 26 0
      desktop/core/src/desktop/js/ko/components/__snapshots__/ko.historyPanel.test.js.snap
  12. 24 0
      desktop/core/src/desktop/js/ko/components/__snapshots__/ko.inlineAutocomplete.test.js.snap
  13. 8 0
      desktop/core/src/desktop/js/ko/components/__snapshots__/ko.jobBrowserLinks.test.js.snap
  14. 1 1
      desktop/core/src/desktop/js/ko/components/__snapshots__/ko.sessionPanel.test.js.snap
  15. 9 1
      desktop/core/src/desktop/js/ko/components/ko.catalogEntriesList.test.js
  16. 3 1
      desktop/core/src/desktop/js/ko/components/ko.contextSelector.js
  17. 28 0
      desktop/core/src/desktop/js/ko/components/ko.contextSelector.test.js
  18. 3 1
      desktop/core/src/desktop/js/ko/components/ko.dropDown.js
  19. 33 0
      desktop/core/src/desktop/js/ko/components/ko.dropDown.test.js
  20. 3 1
      desktop/core/src/desktop/js/ko/components/ko.executionAnalysis.js
  21. 28 0
      desktop/core/src/desktop/js/ko/components/ko.executionAnalysis.test.js
  22. 40 44
      desktop/core/src/desktop/js/ko/components/ko.favoriteApp.js
  23. 65 0
      desktop/core/src/desktop/js/ko/components/ko.favoriteApp.test.js
  24. 3 1
      desktop/core/src/desktop/js/ko/components/ko.fieldSamples.js
  25. 44 0
      desktop/core/src/desktop/js/ko/components/ko.fieldSamples.test.js
  26. 3 1
      desktop/core/src/desktop/js/ko/components/ko.globalSearch.js
  27. 28 0
      desktop/core/src/desktop/js/ko/components/ko.globalSearch.test.js
  28. 3 1
      desktop/core/src/desktop/js/ko/components/ko.historyPanel.js
  29. 28 0
      desktop/core/src/desktop/js/ko/components/ko.historyPanel.test.js
  30. 3 1
      desktop/core/src/desktop/js/ko/components/ko.inlineAutocomplete.js
  31. 32 0
      desktop/core/src/desktop/js/ko/components/ko.inlineAutocomplete.test.js
  32. 3 1
      desktop/core/src/desktop/js/ko/components/ko.jobBrowserLinks.js
  33. 37 0
      desktop/core/src/desktop/js/ko/components/ko.jobBrowserLinks.test.js
  34. 2 1
      package.json

+ 17 - 1
desktop/core/src/desktop/js/api/apiHelper.js

@@ -399,7 +399,7 @@ class ApiHelper {
   /**
    * @param {string} url
    * @param {Object} data
-   * @param {Object} options
+   * @param {Object} [options]
    * @param {function} [options.successCallback]
    * @param {function} [options.errorCallback]
    * @param {boolean} [options.silenceErrors]
@@ -913,6 +913,22 @@ class ApiHelper {
     );
   }
 
+  async fetchFavoriteApp(options) {
+    return new Promise((resolve, reject) => {
+      this.simpleGet('/desktop/api2/user_preferences/default_app')
+        .done(resolve)
+        .fail(reject);
+    });
+  }
+
+  async setFavoriteAppAsync(options) {
+    return new Promise((resolve, reject) => {
+      this.simplePost('/desktop/api2/user_preferences/default_app', options)
+        .done(resolve)
+        .fail(reject);
+    });
+  }
+
   /**
    * @param {Object} options
    * @param {String} options.collectionName

+ 7 - 2
desktop/core/src/desktop/js/jest/jest.init.js

@@ -23,14 +23,19 @@ import './jquery.setup';
 import './sqlTestUtils';
 
 const globalVars = {
-  LOGGED_USERNAME: 'foo',
+  AUTOCOMPLETE_TIMEOUT: 1,
   CACHEABLE_TTL: 1,
   HAS_OPTIMIZER: false,
-  AUTOCOMPLETE_TIMEOUT: 1,
   HUE_I18n: {},
   HUE_BASE_URL: '',
   HUE_CHARTS: {
     TYPES: 'bar'
+  },
+  LOGGED_USERNAME: 'foo',
+  LOGGED_USER_ID: 'bar',
+  STATIC_URLS: {
+    'impala/art/icon_impala_48.png': 'impala/art/icon_impala_48.png',
+    'beeswax/art/icon_beeswax_48.png': 'beeswax/art/icon_beeswax_48.png'
   }
 };
 

+ 19 - 11
desktop/core/src/desktop/js/jest/koTestUtils.js

@@ -41,26 +41,34 @@ export const koSetup = () => {
   });
 
   return {
-    renderComponent: async (name, data) =>
+    renderComponent: async (name, data, instantTimeout) =>
       new Promise((resolve, reject) => {
         const element = window.document.createElement('div');
-        element.setAttribute('data-bind', `component: { name: "${name}", params: $data }`);
+        element.setAttribute(
+          'data-bind',
+          `descendantsComplete: $data.descendantsComplete.bind($data), component: { name: "${name}", params: $data.params }`
+        );
         wrapper.appendChild(element);
 
         try {
           ko.components.get(name, comp => {
             if (comp.createViewModel) {
-              // Instantiate the view model to catch any exception that otherwise would be lost as it's async.
-              // Fire any timeouts right away
               const origSetTimeout = window.setTimeout;
-              window.setTimeout = cb => cb();
-              comp.createViewModel(data, { element: element });
-              window.setTimeout = origSetTimeout;
+              if (instantTimeout) {
+                window.setTimeout = cb => cb();
+                comp.createViewModel(data, { element: element });
+                window.setTimeout = origSetTimeout;
+              } else {
+                comp.createViewModel(data, { element: element });
+              }
 
-              ko.applyBindings(data, wrapper);
-              window.setTimeout(() => {
-                resolve(wrapper);
-              }, 0);
+              ko.applyBindings(
+                {
+                  descendantsComplete: () => resolve(wrapper),
+                  params: data
+                },
+                wrapper
+              );
             } else {
               reject('no createViewModel function found on component ' + name);
             }

+ 119 - 87
desktop/core/src/desktop/js/ko/components/__snapshots__/ko.catalogEntriesList.test.js.snap

@@ -1,7 +1,7 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`ko.catalogEntriesList.js should render component 1`] = `
-"<div data-bind=\\"component: { name: &quot;catalog-entries-list&quot;, params: $data }\\"><script type=\\"text/html\\" id=\\"entries-table-td-description\\">
+"<div data-bind=\\"descendantsComplete: $data.descendantsComplete.bind($data), component: { name: &quot;catalog-entries-list&quot;, params: $data.params }\\"><script type=\\"text/html\\" id=\\"entries-table-td-description\\">
     <td data-bind=\\"attr: { 'title': comment }\\">
     <!-- ko if: $parent.editableDescriptions && !window.HAS_READ_ONLY_CATALOG -->
       <div class=\\"hue-catalog-entries-table-desc\\" data-bind=\\"visibleOnHover: { selector: '.editable-inline-action' }\\">
@@ -64,103 +64,135 @@ exports[`ko.catalogEntriesList.js should render component 1`] = `
           knownFacetValues: knownFacetValues,
           autocompleteFromEntries: autocompleteFromEntries
         }
-      } --><!-- /ko -->
+      } --><div class=\\"inline-autocomp-container\\">
+    <div>
+      <!-- ko if: showMagnify --><!-- /ko-->
+      <form autocomplete=\\"off\\">
+        <input class=\\"inline-autocomp-input\\" autocorrect=\\"off\\" autocomplete=\\"do-not-autocomplete\\" autocapitalize=\\"off\\" spellcheck=\\"false\\" type=\\"text\\" data-bind=\\"
+          attr: { 'placeHolder' : hasFocus() ? '' : placeHolder },
+          textInput: searchInput,
+          hasFocus: hasFocus,
+          clearable: { value: searchInput, onClear: onClear },
+          css: { 'inline-autocomp-magnify-input': showMagnify }\\" placeholder=\\"Filter...\\">
+        <input class=\\"inline-autocomp-autocomp\\" disabled=\\"\\" type=\\"text\\" autocomplete=\\"do-not-autocomplete\\" data-bind=\\"
+          value: inlineAutocomplete,
+          css: { 'inline-autocomp-magnify-input': showMagnify }\\">
+      </form>
+    </div>
+  </div><div class=\\"hue-drop-down-container hue-drop-down-fixed\\" data-bind=\\"event: { 'mousedown': facetDropDownMouseDown }, css: { 'open' : facetDropDownVisible() }, dropDownKeyUp: { onEsc: facetDropDownOnEsc, onEnter: facetDropDownOnEnter, onSelected: facetDropDownOnSelected, dropDownVisible: facetDropDownVisible }\\">
+    <div class=\\"dropdown-menu\\" style=\\"overflow-y: auto; display: none;\\" data-bind=\\"visible: facetSuggestions().length > 0\\">
+      <ul class=\\"hue-inner-drop-down\\" data-bind=\\"foreach: facetSuggestions\\"></ul>
+    </div>
+  </div><!-- /ko -->
     </div>
   </div><div class=\\"catalog-entries-list-container\\">
     <!-- ko hueSpinner: { spin: loading, center: true, size: 'xlarge' } --><!-- /ko -->
-    <!-- ko if: !loading() && catalogEntry().isSource() -->
-    <table id=\\"entryTable\\" class=\\"table table-condensed table-nowrap\\">
-      <thead>
-        <tr>
-          <!-- ko template: 'entries-table-shared-headers' --><!-- /ko -->
-          <th>Database</th>
-          <th>Description <!-- ko if: loadingNav --><i class=\\"fa fa-spinner fa-spin\\"></i><!-- /ko --></th>
-        </tr>
-      </thead>
-      <!-- ko if: filteredEntries().length -->
-      <tbody data-bind=\\"foreach: filteredEntries\\">
-        <tr data-bind=\\"click: onRowClick\\">
-          <!-- ko template: 'entries-table-shared-columns' --><!-- /ko -->
-          <td><a href=\\"javascript: void(0);\\" data-bind=\\"text: catalogEntry().name, click: onClick, attr: { 'title': catalogEntry().getTitle() }\\"></a></td>
-          <!-- ko template: 'entries-table-td-description' --><!-- /ko -->
-        </tr>
-      </tbody>
-      <!-- /ko -->
-      <!-- ko if: filteredEntries().length === 0 -->
-      <!-- ko template: { name: 'entries-table-tbody-no-entries', data: { colCount: 2, hasErrors: hasErrors } } --><!-- /ko -->
-      <!-- /ko -->
-    </table>
-    <!-- /ko -->
+    <!-- ko if: !loading() && catalogEntry().isSource() --><!-- /ko -->
 
-    <!-- ko if: !loading() && catalogEntry().isDatabase() -->
-    <table id=\\"entryTable\\" class=\\"table table-condensed table-nowrap\\">
-      <thead>
-      <tr>
-        <!-- ko template: 'entries-table-shared-headers' --><!-- /ko -->
-        <th data-bind=\\"text: catalogEntry().getSourceType() !== 'solr' ? 'Table' : 'Collection'\\"></th>
-        <th>Description <!-- ko if: loadingNav --><i class=\\"fa fa-spinner fa-spin\\"></i><!-- /ko --></th>
-      </tr>
-      </thead>
-      <!-- ko if: filteredEntries().length -->
-      <tbody data-bind=\\"foreach: filteredEntries\\">
-      <tr data-bind=\\"click: onRowClick\\">
-        <!-- ko template: 'entries-table-shared-columns' --><!-- /ko -->
-        <td><a href=\\"javascript: void(0);\\" data-bind=\\"text: catalogEntry().name, click: onClick, attr: { 'title': catalogEntry().getTitle() }\\"></a></td>
-        <!-- ko template: 'entries-table-td-description' --><!-- /ko -->
-      </tr>
-      </tbody>
-      <!-- /ko -->
-      <!-- ko if: filteredEntries().length === 0 -->
-      <!-- ko template: { name: 'entries-table-tbody-no-entries', data: { colCount: 2, hasErrors: hasErrors } } --><!-- /ko -->
-      <!-- /ko -->
-    </table>
-    <!-- /ko -->
+    <!-- ko if: !loading() && catalogEntry().isDatabase() --><!-- /ko -->
 
-    <!-- ko if: !loading() && (catalogEntry().isTableOrView() || catalogEntry().isComplex()) -->
-    <table class=\\"table table-condensed table-nowrap\\">
-      <thead>
-      <tr>
-        <!-- ko template: 'entries-table-shared-headers' --><!-- /ko -->
-        <th><span data-bind=\\"text: catalogEntry().getSourceType() !== 'solr' ? 'Column' : 'Field'\\"></span> (<span data-bind=\\"text: filteredEntries().length\\"></span>)</th>
-        <th>Type</th>
-        <th with=\\"25%\\">Description <!-- ko if: loadingNav --><i class=\\"fa fa-spinner fa-spin\\"></i><!-- /ko --></th>
-        <th colspan=\\"2\\">Sample <!-- ko if: loadingSamples() || sampleRefreshEnabled() --><i class=\\"fa fa-spinner fa-spin\\"></i><!-- /ko --> <!-- ko if: sampleRefreshEnabled --><a class=\\"inactive-action\\" href=\\"javascript: void(0);\\" data-bind=\\"toggle: sampleRefreshEnabled\\"><i class=\\"fa fa-stop\\"></i></a><!-- /ko --></th>
-      </tr>
-      </thead>
-      <!-- ko if: filteredEntries().length -->
-      <tbody data-bind=\\"foreach: filteredEntries\\">
-      <tr data-bind=\\"click: onRowClick\\">
-        <!-- ko template: 'entries-table-shared-columns' --><!-- /ko -->
-        <td class=\\"name-column\\" data-bind=\\"attr: { 'title': catalogEntry().name + ' - Click for more details' }\\">
-          <a href=\\"javascript: void(0);\\" data-bind=\\"click: onClick\\">
-            <span data-bind=\\"text: catalogEntry().name\\"></span>
-            <!-- ko if: isKey -->
-            &nbsp;<i class=\\"fa fa-key\\" data-bind=\\"tooltip: { title: keyText, html: true }\\"></i>
-            <!-- /ko -->
-            <!-- ko if: popularity && popularity() >= 5 -->
-            &nbsp;<i data-bind=\\"tooltip: { title: 'Popularity: ' + popularity() + '%' }\\" class=\\"fa fa-star-o\\"></i>
-            <!-- /ko -->
-          </a>
-        </td>
-        <td class=\\"type-column\\" data-bind=\\"text: catalogEntry().getType(), attr: { 'title': catalogEntry().getRawType() }\\"></td>
-        <!-- ko template: 'entries-table-td-description' --><!-- /ko -->
-        <td class=\\"sample-column\\" data-bind=\\"html: firstSample, attr: { 'title': hueUtils.html2text(firstSample()) }\\"></td>
-        <td class=\\"sample-column\\" data-bind=\\"html: secondSample, attr: { 'title': hueUtils.html2text(secondSample()) }\\"></td>
-      </tr>
-      </tbody>
-      <!-- /ko -->
-      <!-- ko if: filteredEntries().length === 0 -->
-      <!-- ko template: { name: 'entries-table-tbody-no-entries', data: { colCount: 5, hasErrors: hasErrors } } --><!-- /ko -->
-      <!-- /ko -->
-    </table>
-    <!-- /ko -->
+    <!-- ko if: !loading() && (catalogEntry().isTableOrView() || catalogEntry().isComplex()) --><!-- /ko -->
 
     <!-- ko if: !loading() && catalogEntry().isField() && !catalogEntry().isComplex() -->
     <!-- ko component: { name: 'field-samples', params: {
         catalogEntry: catalogEntry,
         onSampleClick: onSampleClick,
         refreshSampleInterval: refreshSampleInterval
-      }} --><!-- /ko -->
+      }} --><div class=\\"context-popover-inline-autocomplete\\" style=\\"display: flex\\">
+    <div class=\\"context-popover-sample-filter\\">
+      <!-- ko component: {
+        name: 'inline-autocomplete',
+        params: {
+          querySpec: querySpec,
+          autocompleteFromEntries: autocompleteFromEntries
+        }
+      } --><div class=\\"inline-autocomp-container\\">
+    <div>
+      <!-- ko if: showMagnify --><!-- /ko-->
+      <form autocomplete=\\"off\\">
+        <input class=\\"inline-autocomp-input\\" autocorrect=\\"off\\" autocomplete=\\"do-not-autocomplete\\" autocapitalize=\\"off\\" spellcheck=\\"false\\" type=\\"text\\" data-bind=\\"
+          attr: { 'placeHolder' : hasFocus() ? '' : placeHolder },
+          textInput: searchInput,
+          hasFocus: hasFocus,
+          clearable: { value: searchInput, onClear: onClear },
+          css: { 'inline-autocomp-magnify-input': showMagnify }\\" placeholder=\\"Filter...\\">
+        <input class=\\"inline-autocomp-autocomp\\" disabled=\\"\\" type=\\"text\\" autocomplete=\\"do-not-autocomplete\\" data-bind=\\"
+          value: inlineAutocomplete,
+          css: { 'inline-autocomp-magnify-input': showMagnify }\\">
+      </form>
+    </div>
+  </div><div class=\\"hue-drop-down-container hue-drop-down-fixed\\" data-bind=\\"event: { 'mousedown': facetDropDownMouseDown }, css: { 'open' : facetDropDownVisible() }, dropDownKeyUp: { onEsc: facetDropDownOnEsc, onEnter: facetDropDownOnEnter, onSelected: facetDropDownOnSelected, dropDownVisible: facetDropDownVisible }\\">
+    <div class=\\"dropdown-menu\\" style=\\"overflow-y: auto; display: none;\\" data-bind=\\"visible: facetSuggestions().length > 0\\">
+      <ul class=\\"hue-inner-drop-down\\" data-bind=\\"foreach: facetSuggestions\\"></ul>
+    </div>
+  </div><!-- /ko -->
+    </div>
+    <!-- ko if: showOperations -->
+    <div class=\\"context-popover-sample-controls\\">
+      <div class=\\"margin-left-10 inline-block\\" data-bind=\\"component: { name: 'hue-drop-down', params: { value: operation, entries: operations } }\\"><a class=\\"inactive-action hue-drop-down-active\\" href=\\"javascript:void(0)\\" data-bind=\\"toggle: dropDownVisible, css: { 'blue': dropDownVisible }\\">
+    <!-- ko if: icon --><!-- /ko -->
+    <!-- ko if: !noLabel && value -->
+    <span class=\\"hue-drop-down-selected\\" data-bind=\\"text: value() &amp;&amp; typeof value()[labelAttribute] !== 'undefined' ? value()[labelAttribute] : value(), visible: ! dropDownVisible() || !searchable, attr: { 'title': value() &amp;&amp; typeof value()[labelAttribute] !== 'undefined' ? value()[labelAttribute] : value() }\\" title=\\"default\\">default</span>
+    <!-- /ko -->
+    <i class=\\"fa fa-caret-down\\"></i>
+  </a><input class=\\"hue-drop-down-input\\" type=\\"text\\" data-bind=\\"textInput: filter, attr: { 'placeHolder': inputPlaceHolder }, visible: dropDownVisible, style: { color: filterEdited() ? '#000' : '#AAA', 'min-height': '22px', 'margin-left': '10px' }\\" placeholder=\\"default\\" style=\\"display: none; color: rgb(170, 170, 170); min-height: 22px; margin-left: 10px;\\"><i class=\\"fa fa-caret-down\\"></i><div class=\\"hue-drop-down-container\\" data-bind=\\"css: { 'open' : dropDownVisible, 'hue-drop-down-fixed': fixedPosition, 'hue-drop-down-container-searchable': searchable }, dropDownKeyUp: { onEsc: onEsc, onEnter: onEnter, dropDownVisible: dropDownVisible }\\">
+    <div style=\\"overflow-y: auto;\\" class=\\"dropdown-menu\\" data-bind=\\"visible: filteredEntries().length > 0\\">
+      <!-- ko if: foreachVisible --><!-- /ko -->
+      <!-- ko ifnot: foreachVisible -->
+      <ul class=\\"hue-inner-drop-down\\" data-bind=\\"foreach: filteredEntries\\">
+        <!-- ko if: typeof $data.divider !== 'undefined' && $data.divider --><!-- /ko -->
+        <!-- ko if: typeof $data.divider === 'undefined' || !$data.divider -->
+        <li><a href=\\"javascript:void(0)\\" data-bind=\\"text: $data &amp;&amp; typeof $data[$parent.labelAttribute] !== 'undefined' ? $data[$parent.labelAttribute] : $data, click: function () { let previous = $parent.value(); $parent.value($data); $parent.onSelect($data, previous); }\\">default</a></li>
+        <!-- /ko -->
+      
+        <!-- ko if: typeof $data.divider !== 'undefined' && $data.divider --><!-- /ko -->
+        <!-- ko if: typeof $data.divider === 'undefined' || !$data.divider -->
+        <li><a href=\\"javascript:void(0)\\" data-bind=\\"text: $data &amp;&amp; typeof $data[$parent.labelAttribute] !== 'undefined' ? $data[$parent.labelAttribute] : $data, click: function () { let previous = $parent.value(); $parent.value($data); $parent.onSelect($data, previous); }\\">distinct</a></li>
+        <!-- /ko -->
+      
+        <!-- ko if: typeof $data.divider !== 'undefined' && $data.divider --><!-- /ko -->
+        <!-- ko if: typeof $data.divider === 'undefined' || !$data.divider -->
+        <li><a href=\\"javascript:void(0)\\" data-bind=\\"text: $data &amp;&amp; typeof $data[$parent.labelAttribute] !== 'undefined' ? $data[$parent.labelAttribute] : $data, click: function () { let previous = $parent.value(); $parent.value($data); $parent.onSelect($data, previous); }\\">max</a></li>
+        <!-- /ko -->
+      
+        <!-- ko if: typeof $data.divider !== 'undefined' && $data.divider --><!-- /ko -->
+        <!-- ko if: typeof $data.divider === 'undefined' || !$data.divider -->
+        <li><a href=\\"javascript:void(0)\\" data-bind=\\"text: $data &amp;&amp; typeof $data[$parent.labelAttribute] !== 'undefined' ? $data[$parent.labelAttribute] : $data, click: function () { let previous = $parent.value(); $parent.value($data); $parent.onSelect($data, previous); }\\">min</a></li>
+        <!-- /ko -->
+      </ul>
+      <!-- /ko -->
+    </div>
+  </div></div>
+      <div class=\\"margin-left-10 inactive-action inline-block\\">
+        <!-- ko if: loadingSamples() || refreshSampleEnabled() --><!-- /ko -->
+        <!-- ko if: !loadingSamples() && !refreshSampleEnabled() -->
+        <a href=\\"javascript:void(0);\\" data-bind=\\"click: function () { loadSamples(true) }\\"><i class=\\"fa fa-play\\"></i></a>
+        <!-- /ko -->
+      </div>
+    </div>
+    <!-- /ko -->
+  </div><table class=\\"table table-condensed table-nowrap\\">
+    <thead>
+    <tr>
+      <th>Sample</th>
+    </tr>
+    </thead>
+    <!-- ko if: loadingSamples() && (!refreshSampleEnabled() || (refreshSampleEnabled() && columnSamples().length === 0))  --><!-- /ko -->
+
+    <!-- ko if: !loadingSamples() || (refreshSampleEnabled() && columnSamples().length > 0)  -->
+    <tbody data-bind=\\"foreach: filteredColumnSamples\\"></tbody>
+    <!-- ko if: filteredColumnSamples().length === 0 -->
+    <tbody>
+    <tr>
+      <!-- ko ifnot: hasErrors --><!-- /ko -->
+      <!-- ko if: hasErrors -->
+      <td style=\\"font-style: italic;\\">Error loading samples</td>
+      <!-- /ko -->
+    </tr>
+    </tbody>
+    <!-- /ko -->
+    <!-- /ko -->
+  </table><!-- /ko -->
     <!-- /ko -->
   </div></div>"
 `;

+ 10 - 0
desktop/core/src/desktop/js/ko/components/__snapshots__/ko.contextSelector.test.js.snap

@@ -0,0 +1,10 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ko.contextSelector.js should render component 1`] = `
+"<div data-bind=\\"descendantsComplete: $data.descendantsComplete.bind($data), component: { name: &quot;hue-context-selector&quot;, params: $data.params }\\"><i class=\\"fa fa-spinner fa-spin muted\\"></i><div class=\\"inline-block\\" style=\\"\\" data-bind=\\"visible: !loadingContext()\\">
+    <!-- ko if: window.HAS_MULTI_CLUSTER --><!-- /ko -->
+
+    <!-- ko if: availableDatabases().length > 0 && !hideDatabases--><!-- /ko -->
+    <!-- ko if: availableDatabases().length === 0  && !hideDatabases --><!-- /ko -->
+  </div></div>"
+`;

+ 16 - 0
desktop/core/src/desktop/js/ko/components/__snapshots__/ko.dropDown.test.js.snap

@@ -0,0 +1,16 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ko.dropDown.js should render component 1`] = `
+"<div data-bind=\\"descendantsComplete: $data.descendantsComplete.bind($data), component: { name: &quot;hue-drop-down&quot;, params: $data.params }\\"><a class=\\"inactive-action hue-drop-down-active\\" href=\\"javascript:void(0)\\" data-bind=\\"toggle: dropDownVisible, css: { 'blue': dropDownVisible }\\">
+    <!-- ko if: icon --><!-- /ko -->
+    <!-- ko if: !noLabel && value --><!-- /ko -->
+    <i class=\\"fa fa-caret-down\\"></i>
+  </a><input class=\\"hue-drop-down-input\\" type=\\"text\\" data-bind=\\"textInput: filter, attr: { 'placeHolder': inputPlaceHolder }, visible: dropDownVisible, style: { color: filterEdited() ? '#000' : '#AAA', 'min-height': '22px', 'margin-left': '10px' }\\" style=\\"display: none; color: rgb(170, 170, 170); min-height: 22px; margin-left: 10px;\\"><i class=\\"fa fa-caret-down\\"></i><div class=\\"hue-drop-down-container\\" data-bind=\\"css: { 'open' : dropDownVisible, 'hue-drop-down-fixed': fixedPosition, 'hue-drop-down-container-searchable': searchable }, dropDownKeyUp: { onEsc: onEsc, onEnter: onEnter, dropDownVisible: dropDownVisible }\\">
+    <div style=\\"overflow-y: auto; display: none;\\" class=\\"dropdown-menu\\" data-bind=\\"visible: filteredEntries().length > 0\\">
+      <!-- ko if: foreachVisible --><!-- /ko -->
+      <!-- ko ifnot: foreachVisible -->
+      <ul class=\\"hue-inner-drop-down\\" data-bind=\\"foreach: filteredEntries\\"></ul>
+      <!-- /ko -->
+    </div>
+  </div></div>"
+`;

+ 14 - 0
desktop/core/src/desktop/js/ko/components/__snapshots__/ko.executionAnalysis.test.js.snap

@@ -0,0 +1,14 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ko.executionAnalysis.js should render component 1`] = `
+"<div data-bind=\\"descendantsComplete: $data.descendantsComplete.bind($data), component: { name: &quot;hue-execution-analysis&quot;, params: $data.params }\\"><div class=\\"hue-execution-analysis\\">
+    <!-- ko hueSpinner: { spin: loading, inline: true } --><!-- /ko -->
+    <!-- ko ifnot: loading -->
+      <!-- ko if: analysisPossible() && !analysis() -->
+      <div class=\\"no-analysis\\">Execute a query to get query execution analysis.</div>
+      <!-- /ko -->
+      <!-- ko ifnot: analysisPossible --><!-- /ko -->
+      <!-- ko with: analysis --><!-- /ko -->
+    <!-- /ko -->
+  </div></div>"
+`;

+ 13 - 0
desktop/core/src/desktop/js/ko/components/__snapshots__/ko.favoriteApp.test.js.snap

@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ko.favoriteApp.js should render favorite 1`] = `
+"<div data-bind=\\"descendantsComplete: $data.descendantsComplete.bind($data), component: { name: &quot;hue-favorite-app&quot;, params: $data.params }\\"><div class=\\"inline pointer favorite-app\\" data-bind=\\"click: setAsFavoriteApp.bind($data), tooltip: { placement: 'bottom', title: isFavorite() ? 'Unset from default application' : 'Set as default application' }\\">
+    <i class=\\"fa inactive-action fa-star\\" data-bind=\\"css: { 'fa-star-o': !isFavorite(), 'fa-star': isFavorite }\\"></i>
+  </div></div>"
+`;
+
+exports[`ko.favoriteApp.js should render non-favorite 1`] = `
+"<div data-bind=\\"descendantsComplete: $data.descendantsComplete.bind($data), component: { name: &quot;hue-favorite-app&quot;, params: $data.params }\\"><div class=\\"inline pointer favorite-app\\" data-bind=\\"click: setAsFavoriteApp.bind($data), tooltip: { placement: 'bottom', title: isFavorite() ? 'Unset from default application' : 'Set as default application' }\\">
+    <i class=\\"fa inactive-action fa-star-o\\" data-bind=\\"css: { 'fa-star-o': !isFavorite(), 'fa-star': isFavorite }\\"></i>
+  </div></div>"
+`;

+ 99 - 0
desktop/core/src/desktop/js/ko/components/__snapshots__/ko.fieldSamples.test.js.snap

@@ -0,0 +1,99 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ko.fieldSamples.js should render component 1`] = `
+"<div data-bind=\\"descendantsComplete: $data.descendantsComplete.bind($data), component: { name: &quot;field-samples&quot;, params: $data.params }\\"><div class=\\"context-popover-inline-autocomplete\\" style=\\"display: flex\\">
+    <div class=\\"context-popover-sample-filter\\">
+      <!-- ko component: {
+        name: 'inline-autocomplete',
+        params: {
+          querySpec: querySpec,
+          autocompleteFromEntries: autocompleteFromEntries
+        }
+      } --><div class=\\"inline-autocomp-container\\">
+    <div>
+      <!-- ko if: showMagnify --><!-- /ko-->
+      <form autocomplete=\\"off\\">
+        <input class=\\"inline-autocomp-input\\" autocorrect=\\"off\\" autocomplete=\\"do-not-autocomplete\\" autocapitalize=\\"off\\" spellcheck=\\"false\\" type=\\"text\\" data-bind=\\"
+          attr: { 'placeHolder' : hasFocus() ? '' : placeHolder },
+          textInput: searchInput,
+          hasFocus: hasFocus,
+          clearable: { value: searchInput, onClear: onClear },
+          css: { 'inline-autocomp-magnify-input': showMagnify }\\" placeholder=\\"Filter...\\">
+        <input class=\\"inline-autocomp-autocomp\\" disabled=\\"\\" type=\\"text\\" autocomplete=\\"do-not-autocomplete\\" data-bind=\\"
+          value: inlineAutocomplete,
+          css: { 'inline-autocomp-magnify-input': showMagnify }\\">
+      </form>
+    </div>
+  </div><div class=\\"hue-drop-down-container hue-drop-down-fixed\\" data-bind=\\"event: { 'mousedown': facetDropDownMouseDown }, css: { 'open' : facetDropDownVisible() }, dropDownKeyUp: { onEsc: facetDropDownOnEsc, onEnter: facetDropDownOnEnter, onSelected: facetDropDownOnSelected, dropDownVisible: facetDropDownVisible }\\">
+    <div class=\\"dropdown-menu\\" style=\\"overflow-y: auto; display: none;\\" data-bind=\\"visible: facetSuggestions().length > 0\\">
+      <ul class=\\"hue-inner-drop-down\\" data-bind=\\"foreach: facetSuggestions\\"></ul>
+    </div>
+  </div><!-- /ko -->
+    </div>
+    <!-- ko if: showOperations -->
+    <div class=\\"context-popover-sample-controls\\">
+      <div class=\\"margin-left-10 inline-block\\" data-bind=\\"component: { name: 'hue-drop-down', params: { value: operation, entries: operations } }\\"><a class=\\"inactive-action hue-drop-down-active\\" href=\\"javascript:void(0)\\" data-bind=\\"toggle: dropDownVisible, css: { 'blue': dropDownVisible }\\">
+    <!-- ko if: icon --><!-- /ko -->
+    <!-- ko if: !noLabel && value -->
+    <span class=\\"hue-drop-down-selected\\" data-bind=\\"text: value() &amp;&amp; typeof value()[labelAttribute] !== 'undefined' ? value()[labelAttribute] : value(), visible: ! dropDownVisible() || !searchable, attr: { 'title': value() &amp;&amp; typeof value()[labelAttribute] !== 'undefined' ? value()[labelAttribute] : value() }\\" title=\\"default\\">default</span>
+    <!-- /ko -->
+    <i class=\\"fa fa-caret-down\\"></i>
+  </a><input class=\\"hue-drop-down-input\\" type=\\"text\\" data-bind=\\"textInput: filter, attr: { 'placeHolder': inputPlaceHolder }, visible: dropDownVisible, style: { color: filterEdited() ? '#000' : '#AAA', 'min-height': '22px', 'margin-left': '10px' }\\" placeholder=\\"default\\" style=\\"display: none; color: rgb(170, 170, 170); min-height: 22px; margin-left: 10px;\\"><i class=\\"fa fa-caret-down\\"></i><div class=\\"hue-drop-down-container\\" data-bind=\\"css: { 'open' : dropDownVisible, 'hue-drop-down-fixed': fixedPosition, 'hue-drop-down-container-searchable': searchable }, dropDownKeyUp: { onEsc: onEsc, onEnter: onEnter, dropDownVisible: dropDownVisible }\\">
+    <div style=\\"overflow-y: auto;\\" class=\\"dropdown-menu\\" data-bind=\\"visible: filteredEntries().length > 0\\">
+      <!-- ko if: foreachVisible --><!-- /ko -->
+      <!-- ko ifnot: foreachVisible -->
+      <ul class=\\"hue-inner-drop-down\\" data-bind=\\"foreach: filteredEntries\\">
+        <!-- ko if: typeof $data.divider !== 'undefined' && $data.divider --><!-- /ko -->
+        <!-- ko if: typeof $data.divider === 'undefined' || !$data.divider -->
+        <li><a href=\\"javascript:void(0)\\" data-bind=\\"text: $data &amp;&amp; typeof $data[$parent.labelAttribute] !== 'undefined' ? $data[$parent.labelAttribute] : $data, click: function () { let previous = $parent.value(); $parent.value($data); $parent.onSelect($data, previous); }\\">default</a></li>
+        <!-- /ko -->
+      
+        <!-- ko if: typeof $data.divider !== 'undefined' && $data.divider --><!-- /ko -->
+        <!-- ko if: typeof $data.divider === 'undefined' || !$data.divider -->
+        <li><a href=\\"javascript:void(0)\\" data-bind=\\"text: $data &amp;&amp; typeof $data[$parent.labelAttribute] !== 'undefined' ? $data[$parent.labelAttribute] : $data, click: function () { let previous = $parent.value(); $parent.value($data); $parent.onSelect($data, previous); }\\">distinct</a></li>
+        <!-- /ko -->
+      
+        <!-- ko if: typeof $data.divider !== 'undefined' && $data.divider --><!-- /ko -->
+        <!-- ko if: typeof $data.divider === 'undefined' || !$data.divider -->
+        <li><a href=\\"javascript:void(0)\\" data-bind=\\"text: $data &amp;&amp; typeof $data[$parent.labelAttribute] !== 'undefined' ? $data[$parent.labelAttribute] : $data, click: function () { let previous = $parent.value(); $parent.value($data); $parent.onSelect($data, previous); }\\">max</a></li>
+        <!-- /ko -->
+      
+        <!-- ko if: typeof $data.divider !== 'undefined' && $data.divider --><!-- /ko -->
+        <!-- ko if: typeof $data.divider === 'undefined' || !$data.divider -->
+        <li><a href=\\"javascript:void(0)\\" data-bind=\\"text: $data &amp;&amp; typeof $data[$parent.labelAttribute] !== 'undefined' ? $data[$parent.labelAttribute] : $data, click: function () { let previous = $parent.value(); $parent.value($data); $parent.onSelect($data, previous); }\\">min</a></li>
+        <!-- /ko -->
+      </ul>
+      <!-- /ko -->
+    </div>
+  </div></div>
+      <div class=\\"margin-left-10 inactive-action inline-block\\">
+        <!-- ko if: loadingSamples() || refreshSampleEnabled() --><!-- /ko -->
+        <!-- ko if: !loadingSamples() && !refreshSampleEnabled() -->
+        <a href=\\"javascript:void(0);\\" data-bind=\\"click: function () { loadSamples(true) }\\"><i class=\\"fa fa-play\\"></i></a>
+        <!-- /ko -->
+      </div>
+    </div>
+    <!-- /ko -->
+  </div><table class=\\"table table-condensed table-nowrap\\">
+    <thead>
+    <tr>
+      <th>Sample</th>
+    </tr>
+    </thead>
+    <!-- ko if: loadingSamples() && (!refreshSampleEnabled() || (refreshSampleEnabled() && columnSamples().length === 0))  --><!-- /ko -->
+
+    <!-- ko if: !loadingSamples() || (refreshSampleEnabled() && columnSamples().length > 0)  -->
+    <tbody data-bind=\\"foreach: filteredColumnSamples\\"></tbody>
+    <!-- ko if: filteredColumnSamples().length === 0 -->
+    <tbody>
+    <tr>
+      <!-- ko ifnot: hasErrors --><!-- /ko -->
+      <!-- ko if: hasErrors -->
+      <td style=\\"font-style: italic;\\">Error loading samples</td>
+      <!-- /ko -->
+    </tr>
+    </tbody>
+    <!-- /ko -->
+    <!-- /ko -->
+  </table></div>"
+`;

+ 29 - 0
desktop/core/src/desktop/js/ko/components/__snapshots__/ko.globalSearch.test.js.snap

@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ko.globalSearch.js should render component 1`] = `
+"<div data-bind=\\"descendantsComplete: $data.descendantsComplete.bind($data), component: { name: &quot;hue-global-search&quot;, params: $data.params }\\"><script type=\\"text/html\\" id=\\"top-search-autocomp-item\\">
+    <a href=\\"javascript:void(0);\\">
+      <div>
+        <div><i class=\\"fa fa-fw\\" data-bind=\\"css: icon\\"></i></div>
+        <div>
+          <div data-bind=\\"html: label, style: { 'padding-top': description ? 0 : '9px' }\\"></div>
+          <!-- ko if: description -->
+          <div data-bind=\\"html: description\\"></div>
+          <!-- /ko -->
+        </div>
+      </div>
+    </a>
+  </script><script type=\\"text/html\\" id=\\"top-search-autocomp-no-match\\">
+    <div style=\\"height: 30px;\\">
+      <div>No match found</div>
+    </div>
+  </script><div class=\\"global-search-results global-search-empty\\" data-bind=\\"onClickOutside: onResultClickOutside\\">
+    <div>No results found.</div>
+  </div><div class=\\"global-search-results\\" data-bind=\\"onClickOutside: onResultClickOutside, style: { 'height' : heightWhenDragging }\\">
+    <div class=\\"global-search-alternatives global-search-full-width\\" data-bind=\\"css: { 'global-search-full-width': !selectedResult() }, delayedOverflow\\" style=\\"position: relative\\">
+      <!-- ko foreach: searchResultCategories --><!-- /ko -->
+      <!-- ko hueSpinner: { spin: loading() && searchResultCategories().length > 0, inline: true } --><!-- /ko -->
+    </div>
+    <!-- ko with: selectedResult --><!--/ko -->
+  </div></div>"
+`;

+ 26 - 0
desktop/core/src/desktop/js/ko/components/__snapshots__/ko.historyPanel.test.js.snap

@@ -0,0 +1,26 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ko.historyPanel.js should render component 1`] = `
+"<div data-bind=\\"descendantsComplete: $data.descendantsComplete.bind($data), component: { name: &quot;hue-history-panel&quot;, params: $data.params }\\"><button class=\\"btn btn-flat pull-right btn-toggle-jobs-panel\\" title=\\"Task History\\" data-bind=\\"click: toggleVisibility\\">
+    <i class=\\"fa fa-history\\"></i>
+    <div class=\\"jobs-badge\\" data-bind=\\"text: historyRunningJobs().length, visible: historyRunningJobs().length > 0\\" style=\\"display: none;\\">0</div>
+  </button><div class=\\"jobs-panel history-panel\\" data-bind=\\"visible: historyPanelVisible, style: { 'top' : top, 'left': left }\\" style=\\"display: none;\\">
+    <a class=\\"pointer inactive-action pull-right\\" data-bind=\\"click: function(){ historyPanelVisible(false); }\\"><i class=\\"fa fa-fw fa-times\\"></i></a>
+    <!-- ko ifnot: editorViewModel.selectedNotebook() && editorViewModel.selectedNotebook().history().length > 0 -->
+      <span style=\\"font-style: italic\\">No task history.</span>
+    <!-- /ko -->
+    <!-- ko if: editorViewModel.selectedNotebook() && editorViewModel.selectedNotebook().history().length > 0 --><!-- /ko -->
+  </div><div id=\\"clearNotificationHistoryModal\\" class=\\"modal hide fade\\" data-backdrop=\\"false\\">
+    <div class=\\"modal-header\\">
+      <button type=\\"button\\" class=\\"close\\" data-dismiss=\\"modal\\" aria-label=\\"Close\\"><span aria-hidden=\\"true\\">×</span></button>
+      <h2 class=\\"modal-title\\">Confirm History Clearing</h2>
+    </div>
+    <div class=\\"modal-body\\">
+      <p>Are you sure you want to clear the task history?</p>
+    </div>
+    <div class=\\"modal-footer\\">
+      <a class=\\"btn\\" data-dismiss=\\"modal\\">No</a>
+      <a class=\\"btn btn-danger disable-feedback\\" data-bind=\\"click: function() { editorViewModel.selectedNotebook().clearHistory(); editorViewModel.selectedNotebook(null); }\\">Yes</a>
+    </div>
+  </div></div>"
+`;

+ 24 - 0
desktop/core/src/desktop/js/ko/components/__snapshots__/ko.inlineAutocomplete.test.js.snap

@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ko.inlineAutocomplete.js should render component 1`] = `
+"<div data-bind=\\"descendantsComplete: $data.descendantsComplete.bind($data), component: { name: &quot;inline-autocomplete&quot;, params: $data.params }\\"><div class=\\"inline-autocomp-container\\">
+    <div>
+      <!-- ko if: showMagnify --><!-- /ko-->
+      <form autocomplete=\\"off\\">
+        <input class=\\"inline-autocomp-input\\" autocorrect=\\"off\\" autocomplete=\\"do-not-autocomplete\\" autocapitalize=\\"off\\" spellcheck=\\"false\\" type=\\"text\\" data-bind=\\"
+          attr: { 'placeHolder' : hasFocus() ? '' : placeHolder },
+          textInput: searchInput,
+          hasFocus: hasFocus,
+          clearable: { value: searchInput, onClear: onClear },
+          css: { 'inline-autocomp-magnify-input': showMagnify }\\" placeholder=\\"Filter...\\">
+        <input class=\\"inline-autocomp-autocomp\\" disabled=\\"\\" type=\\"text\\" autocomplete=\\"do-not-autocomplete\\" data-bind=\\"
+          value: inlineAutocomplete,
+          css: { 'inline-autocomp-magnify-input': showMagnify }\\">
+      </form>
+    </div>
+  </div><div class=\\"hue-drop-down-container hue-drop-down-fixed\\" data-bind=\\"event: { 'mousedown': facetDropDownMouseDown }, css: { 'open' : facetDropDownVisible() }, dropDownKeyUp: { onEsc: facetDropDownOnEsc, onEnter: facetDropDownOnEnter, onSelected: facetDropDownOnSelected, dropDownVisible: facetDropDownVisible }\\">
+    <div class=\\"dropdown-menu\\" style=\\"overflow-y: auto; display: none;\\" data-bind=\\"visible: facetSuggestions().length > 0\\">
+      <ul class=\\"hue-inner-drop-down\\" data-bind=\\"foreach: facetSuggestions\\"></ul>
+    </div>
+  </div></div>"
+`;

+ 8 - 0
desktop/core/src/desktop/js/ko/components/__snapshots__/ko.jobBrowserLinks.test.js.snap

@@ -0,0 +1,8 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ko.jobBrowserLinks.js should render component 1`] = `
+"<div data-bind=\\"descendantsComplete: $data.descendantsComplete.bind($data), component: { name: &quot;hue-job-browser-links&quot;, params: $data.params }\\"><div class=\\"btn-group pull-right\\">
+    <!-- ko if: window.IS_K8S_ONLY --><!-- /ko -->
+    <!-- ko if: window.HAS_JOB_BROWSER --><!-- /ko -->
+  </div></div>"
+`;

+ 1 - 1
desktop/core/src/desktop/js/ko/components/__snapshots__/ko.sessionPanel.test.js.snap

@@ -1,7 +1,7 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`ko.sessionPanel.js should render component 1`] = `
-"<div data-bind=\\"component: { name: &quot;session-panel&quot;, params: $data }\\"><div class=\\"session-panel\\" data-test=\\"session-panel\\" data-bind=\\"slideVisible: visible\\" style=\\"display: none;\\">
+"<div data-bind=\\"descendantsComplete: $data.descendantsComplete.bind($data), component: { name: &quot;session-panel&quot;, params: $data.params }\\"><div class=\\"session-panel\\" data-test=\\"session-panel\\" data-bind=\\"slideVisible: visible\\" style=\\"display: none;\\">
   <div class=\\"margin-top-10 padding-left-10 padding-right-10\\">
     <h4 class=\\"margin-bottom-10\\"><i class=\\"fa fa-cogs\\"></i> Sessions</h4>
     <div class=\\"session-panel-content\\">

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

@@ -18,6 +18,10 @@ import $ from 'jquery';
 import { koSetup } from 'jest/koTestUtils';
 import { NAME } from './ko.catalogEntriesList';
 
+import 'ko/components/ko.inlineAutocomplete';
+import 'ko/components/ko.fieldSamples';
+import 'ko/components/ko.dropDown';
+
 describe('ko.catalogEntriesList.js', () => {
   const setup = koSetup();
 
@@ -26,10 +30,14 @@ describe('ko.catalogEntriesList.js', () => {
       catalogEntry: {
         isField: () => true,
         isComplex: () => false,
+        isDatabase: () => false,
         getChildren: () => $.Deferred().resolve([]),
+        getSample: () => $.Deferred().reject(),
+        getSourceType: () => 'impala',
         loadNavigatorMetaForChildren: () => $.Deferred().reject(),
         loadNavOptPopularityForChildren: () => $.Deferred().reject(),
-        isTableOrView: () => false
+        isTableOrView: () => false,
+        isSource: () => false
       }
     });
 

+ 3 - 1
desktop/core/src/desktop/js/ko/components/ko.contextSelector.js

@@ -24,6 +24,8 @@ import dataCatalog from 'catalog/dataCatalog';
 import huePubSub from 'utils/huePubSub';
 import I18n from 'utils/i18n';
 
+export const NAME = 'hue-context-selector';
+
 const TEMPLATE = `
   <!-- ko if: loadingContext -->
   <i class="fa fa-spinner fa-spin muted"></i>
@@ -478,4 +480,4 @@ HueContextSelector.prototype.dispose = function() {
   }
 };
 
-componentUtils.registerComponent('hue-context-selector', HueContextSelector, TEMPLATE);
+componentUtils.registerComponent(NAME, HueContextSelector, TEMPLATE);

+ 28 - 0
desktop/core/src/desktop/js/ko/components/ko.contextSelector.test.js

@@ -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 { koSetup } from 'jest/koTestUtils';
+import { NAME } from './ko.contextSelector';
+
+describe('ko.contextSelector.js', () => {
+  const setup = koSetup();
+
+  it('should render component', async () => {
+    const element = await setup.renderComponent(NAME, {});
+
+    expect(element.innerHTML).toMatchSnapshot();
+  });
+});

+ 3 - 1
desktop/core/src/desktop/js/ko/components/ko.dropDown.js

@@ -20,6 +20,8 @@ import ko from 'knockout';
 import componentUtils from './componentUtils';
 import I18n from 'utils/i18n';
 
+export const NAME = 'hue-drop-down';
+
 const TEMPLATE = `
   <!-- ko if: !menuOnly && (!dropDownVisible() || !searchable) -->
   <a class="inactive-action hue-drop-down-active" href="javascript:void(0)" data-bind="toggle: dropDownVisible, css: { 'blue': dropDownVisible }">
@@ -256,7 +258,7 @@ class HueDropDown {
 }
 
 componentUtils.registerComponent(
-  'hue-drop-down',
+  NAME,
   {
     createViewModel: function(params, componentInfo) {
       return new HueDropDown(params, componentInfo.element);

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

@@ -0,0 +1,33 @@
+// 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 ko from 'knockout';
+
+import { koSetup } from 'jest/koTestUtils';
+import { NAME } from './ko.dropDown';
+
+describe('ko.dropDown.js', () => {
+  const setup = koSetup();
+
+  it('should render component', async () => {
+    const element = await setup.renderComponent(NAME, {
+      value: ko.observable(),
+      entries: ko.observableArray()
+    });
+
+    expect(element.innerHTML).toMatchSnapshot();
+  });
+});

+ 3 - 1
desktop/core/src/desktop/js/ko/components/ko.executionAnalysis.js

@@ -25,6 +25,8 @@ import componentUtils from './componentUtils';
 import huePubSub from 'utils/huePubSub';
 import I18n from 'utils/i18n';
 
+export const NAME = 'hue-execution-analysis';
+
 const TEMPLATE = `
   <div class="hue-execution-analysis">
     <!-- ko hueSpinner: { spin: loading, inline: true } --><!-- /ko -->
@@ -289,4 +291,4 @@ class ExecutionAnalysis {
   }
 }
 
-componentUtils.registerComponent('hue-execution-analysis', ExecutionAnalysis, TEMPLATE);
+componentUtils.registerComponent(NAME, ExecutionAnalysis, TEMPLATE);

+ 28 - 0
desktop/core/src/desktop/js/ko/components/ko.executionAnalysis.test.js

@@ -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 { koSetup } from 'jest/koTestUtils';
+import { NAME } from './ko.executionAnalysis';
+
+describe('ko.executionAnalysis.js', () => {
+  const setup = koSetup();
+
+  it('should render component', async () => {
+    const element = await setup.renderComponent(NAME, {});
+
+    expect(element.innerHTML).toMatchSnapshot();
+  });
+});

+ 40 - 44
desktop/core/src/desktop/js/ko/components/ko.favoriteApp.js

@@ -14,15 +14,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import $ from 'jquery';
 import ko from 'knockout';
 
+import ApiHelper from 'api/apiHelper';
 import componentUtils from './componentUtils';
 import huePubSub from 'utils/huePubSub';
 import I18n from 'utils/i18n';
 
+export const NAME = 'hue-favorite-app';
+
 const TEMPLATE = `
-  <div class="inline pointer favorite-app" data-bind="click: setAsFavoriteApp, tooltip: { placement: 'bottom', title: isFavorite() ? '${I18n(
+  <div class="inline pointer favorite-app" data-bind="click: setAsFavoriteApp.bind($data), tooltip: { placement: 'bottom', title: isFavorite() ? '${I18n(
     'Unset from default application'
   )}' : '${I18n('Set as default application')}' }">
     <i class="fa inactive-action" data-bind="css: { 'fa-star-o': !isFavorite(), 'fa-star': isFavorite }"></i>
@@ -31,55 +33,49 @@ const TEMPLATE = `
 
 class FavoriteApp {
   constructor(params) {
-    const self = this;
-    self.isFavorite = ko.observable(false);
-    self.app = params.app;
-    self.interpreter = params.interpreter;
+    this.isFavorite = ko.observable(false);
+    this.app = params.app;
+    this.interpreter = params.interpreter;
+
+    ApiHelper.fetchFavoriteApp().then(this.updateFromApiResponse.bind(this));
+  }
 
-    self.parseCurrentFavorite = function(data, announce) {
-      self.isFavorite(false);
-      if (data.status === 0 && data.data && data.data.default_app) {
-        try {
-          const defaultApp = JSON.parse(data.data.default_app);
-          self.isFavorite(
-            defaultApp.app === self.app &&
-              ((self.app === 'editor' && defaultApp.interpreter === self.interpreter) ||
-                self.app !== 'editor')
-          );
-          if (announce) {
-            huePubSub.publish('hue.new.default.app', defaultApp);
-          }
-        } catch (e) {
-          console.error('There was a problem decoding the default app setting.');
+  updateFromApiResponse(data, announce) {
+    this.isFavorite(false);
+    if (data.status === 0 && data.data && data.data.default_app) {
+      try {
+        const defaultApp = JSON.parse(data.data.default_app);
+        this.isFavorite(
+          defaultApp.app === this.app &&
+            ((this.app === 'editor' && defaultApp.interpreter === this.interpreter) ||
+              this.app !== 'editor')
+        );
+        if (announce) {
+          huePubSub.publish('hue.new.default.app', defaultApp);
         }
+      } catch (e) {
+        console.error('There was a problem decoding the default app setting.');
       }
-    };
+    }
+  }
 
-    self.setAsFavoriteApp = function(vm, e) {
-      e.originalEvent.stopPropagation();
-      e.originalEvent.stopImmediatePropagation();
+  async setAsFavoriteApp(vm, e) {
+    e.originalEvent.stopPropagation();
+    e.originalEvent.stopImmediatePropagation();
+    const post = {};
+    if (this.isFavorite()) {
+      post.delete = true;
+    } else {
       const postParams = {
-        app: self.app
+        app: this.app
       };
-      if (self.interpreter !== '') {
-        postParams['interpreter'] = self.interpreter;
-      }
-      const post = {};
-      if (self.isFavorite()) {
-        post['delete'] = true;
-      } else {
-        post['set'] = ko.mapping.toJSON(postParams);
+      if (this.interpreter !== '') {
+        postParams.interpreter = this.interpreter;
       }
-      $.post('/desktop/api2/user_preferences/default_app', post, data => {
-        self.parseCurrentFavorite(data, true);
-      });
-    };
-
-    // Load the fav app status
-    $.get('/desktop/api2/user_preferences/default_app', data => {
-      self.parseCurrentFavorite(data);
-    });
+      post.set = JSON.stringify(postParams);
+    }
+    this.updateFromApiResponse(await ApiHelper.setFavoriteAppAsync(post), true);
   }
 }
 
-componentUtils.registerComponent('hue-favorite-app', FavoriteApp, TEMPLATE);
+componentUtils.registerComponent(NAME, FavoriteApp, TEMPLATE);

+ 65 - 0
desktop/core/src/desktop/js/ko/components/ko.favoriteApp.test.js

@@ -0,0 +1,65 @@
+// 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 ApiHelper from 'api/apiHelper';
+import { koSetup } from 'jest/koTestUtils';
+import { NAME } from './ko.favoriteApp';
+
+describe('ko.favoriteApp.js', () => {
+  const setup = koSetup();
+
+  it('should render non-favorite', async () => {
+    const apiPromise = new Promise(resolve => {
+      jest.spyOn(ApiHelper, 'fetchFavoriteApp').mockImplementation(async () => {
+        resolve();
+        return Promise.resolve({
+          status: 0,
+          data: {
+            default_app: JSON.stringify({ app: 'bar' })
+          }
+        });
+      });
+    });
+    const element = await setup.renderComponent(NAME, {
+      app: 'foo'
+    });
+
+    await apiPromise;
+    // await sleep(100);
+    expect(element.innerHTML).toMatchSnapshot();
+  });
+
+  it('should render favorite', async () => {
+    const apiPromise = new Promise(resolve => {
+      jest.spyOn(ApiHelper, 'fetchFavoriteApp').mockImplementation(async () => {
+        resolve();
+        return Promise.resolve({
+          status: 0,
+          data: {
+            default_app: JSON.stringify({ app: 'foo' })
+          }
+        });
+      });
+    });
+    const element = await setup.renderComponent(NAME, {
+      app: 'foo'
+    });
+
+    await apiPromise;
+    // await sleep(100);
+    expect(element.innerHTML).toMatchSnapshot();
+  });
+});

+ 3 - 1
desktop/core/src/desktop/js/ko/components/ko.fieldSamples.js

@@ -21,6 +21,8 @@ import huePubSub from 'utils/huePubSub';
 import hueUtils from 'utils/hueUtils';
 import I18n from 'utils/i18n';
 
+export const NAME = 'field-samples';
+
 const TEMPLATE = `
   <div class="context-popover-inline-autocomplete" style="display: flex">
     <div class="context-popover-sample-filter">
@@ -232,4 +234,4 @@ class FieldSamples {
   }
 }
 
-componentUtils.registerComponent('field-samples', FieldSamples, TEMPLATE);
+componentUtils.registerComponent(NAME, FieldSamples, TEMPLATE);

+ 44 - 0
desktop/core/src/desktop/js/ko/components/ko.fieldSamples.test.js

@@ -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 $ from 'jquery';
+
+import { koSetup } from 'jest/koTestUtils';
+import { NAME } from './ko.fieldSamples';
+
+import 'ko/components/ko.inlineAutocomplete';
+import 'ko/components/ko.dropDown';
+
+describe('ko.fieldSamples.js', () => {
+  const setup = koSetup();
+
+  it('should render component', async () => {
+    const element = await setup.renderComponent(NAME, {
+      catalogEntry: {
+        isField: () => true,
+        isComplex: () => false,
+        getChildren: () => $.Deferred().resolve([]),
+        getSample: () => $.Deferred().reject(),
+        loadNavigatorMetaForChildren: () => $.Deferred().reject(),
+        loadNavOptPopularityForChildren: () => $.Deferred().reject(),
+        isTableOrView: () => false,
+        getSourceType: () => 'impala'
+      }
+    });
+
+    expect(element.innerHTML).toMatchSnapshot();
+  });
+});

+ 3 - 1
desktop/core/src/desktop/js/ko/components/ko.globalSearch.js

@@ -23,6 +23,8 @@ import componentUtils from './componentUtils';
 import huePubSub from 'utils/huePubSub';
 import I18n from 'utils/i18n';
 
+export const NAME = 'hue-global-search';
+
 const TEMPLATE = `
   <script type="text/html" id="top-search-autocomp-item">
     <a href="javascript:void(0);">
@@ -596,4 +598,4 @@ class GlobalSearch {
   }
 }
 
-componentUtils.registerComponent('hue-global-search', GlobalSearch, TEMPLATE);
+componentUtils.registerComponent(NAME, GlobalSearch, TEMPLATE);

+ 28 - 0
desktop/core/src/desktop/js/ko/components/ko.globalSearch.test.js

@@ -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 { koSetup } from 'jest/koTestUtils';
+import { NAME } from './ko.globalSearch';
+
+describe('ko.globalSearch.js', () => {
+  const setup = koSetup();
+
+  it('should render component', async () => {
+    const element = await setup.renderComponent(NAME, {});
+
+    expect(element.innerHTML).toMatchSnapshot();
+  });
+});

+ 3 - 1
desktop/core/src/desktop/js/ko/components/ko.historyPanel.js

@@ -23,6 +23,8 @@ import componentUtils from './componentUtils';
 import huePubSub from 'utils/huePubSub';
 import I18n from 'utils/i18n';
 
+export const NAME = 'hue-history-panel';
+
 const TEMPLATE = `
   <button class="btn btn-flat pull-right btn-toggle-jobs-panel" title="${I18n(
     'Task History'
@@ -315,4 +317,4 @@ class HistoryPanel {
   }
 }
 
-componentUtils.registerComponent('hue-history-panel', HistoryPanel, TEMPLATE);
+componentUtils.registerComponent(NAME, HistoryPanel, TEMPLATE);

+ 28 - 0
desktop/core/src/desktop/js/ko/components/ko.historyPanel.test.js

@@ -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 { koSetup } from 'jest/koTestUtils';
+import { NAME } from './ko.historyPanel';
+
+describe('ko.historyPanel.js', () => {
+  const setup = koSetup();
+
+  it('should render component', async () => {
+    const element = await setup.renderComponent(NAME, {});
+
+    expect(element.innerHTML).toMatchSnapshot();
+  });
+});

+ 3 - 1
desktop/core/src/desktop/js/ko/components/ko.inlineAutocomplete.js

@@ -22,6 +22,8 @@ import globalSearchParser from 'parse/globalSearchParser';
 import hueDebug from 'utils/hueDebug';
 import I18n from 'utils/i18n';
 
+export const NAME = 'inline-autocomplete';
+
 const TEMPLATE = `
   <div class="inline-autocomp-container">
     <div>
@@ -440,4 +442,4 @@ class InlineAutocomplete {
   }
 }
 
-componentUtils.registerComponent('inline-autocomplete', InlineAutocomplete, TEMPLATE);
+componentUtils.registerComponent(NAME, InlineAutocomplete, TEMPLATE);

+ 32 - 0
desktop/core/src/desktop/js/ko/components/ko.inlineAutocomplete.test.js

@@ -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.
+
+import ko from 'knockout';
+
+import { koSetup } from 'jest/koTestUtils';
+import { NAME } from './ko.inlineAutocomplete';
+
+describe('ko.inlineAutocomplete.js', () => {
+  const setup = koSetup();
+
+  it('should render component', async () => {
+    const element = await setup.renderComponent(NAME, {
+      querySpec: ko.observable()
+    });
+
+    expect(element.innerHTML).toMatchSnapshot();
+  });
+});

+ 3 - 1
desktop/core/src/desktop/js/ko/components/ko.jobBrowserLinks.js

@@ -21,6 +21,8 @@ import componentUtils from './componentUtils';
 import huePubSub from 'utils/huePubSub';
 import I18n from 'utils/i18n';
 
+export const NAME = 'hue-job-browser-links';
+
 const TEMPLATE = `
   <div class="btn-group pull-right">
     <!-- ko if: window.IS_K8S_ONLY -->
@@ -190,4 +192,4 @@ class JobBrowserPanel {
   }
 }
 
-componentUtils.registerComponent('hue-job-browser-links', JobBrowserPanel, TEMPLATE);
+componentUtils.registerComponent(NAME, JobBrowserPanel, TEMPLATE);

+ 37 - 0
desktop/core/src/desktop/js/ko/components/ko.jobBrowserLinks.test.js

@@ -0,0 +1,37 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import { koSetup } from 'jest/koTestUtils';
+import { NAME } from './ko.jobBrowserLinks';
+
+describe('ko.jobBrowserLinks.js', () => {
+  const setup = koSetup();
+
+  it('should render component', async () => {
+    jest.spyOn($, 'post').mockImplementation(url => {
+      if (url === '/jobbrowser/jobs/') {
+        return $.Deferred()
+          .resolve({ jobs: [] })
+          .promise();
+      }
+    });
+
+    const element = await setup.renderComponent(NAME, {});
+
+    expect(element.innerHTML).toMatchSnapshot();
+  });
+});

+ 2 - 1
package.json

@@ -109,7 +109,8 @@
     "lint-fix": "npm run lint -- --fix",
     "test": "jest",
     "test-coverage": "jest --coverage",
-    "test-dev": "jest --watch"
+    "test-dev": "jest --watch",
+    "test-clearCache": "jest --clearCache"
   },
   "files": []
 }