Sfoglia il codice sorgente

HUE-8687 [frontend] Modularize all custom ko bindings

Johan Ahlen 7 anni fa
parent
commit
ea80b4dd5f
100 ha cambiato i file con 8131 aggiunte e 196 eliminazioni
  1. 4 3
      .eslintrc.js
  2. 0 18
      apps/beeswax/src/beeswax/static/beeswax/js/beeswax.vm.js
  3. 2 2
      apps/beeswax/src/beeswax/templates/execute.mako
  4. 1 23
      apps/beeswax/src/beeswax/templates/save_results.mako
  5. 1 21
      apps/beeswax/src/beeswax/templates/watch_results.mako
  6. 3 48
      apps/jobsub/src/jobsub/static/jobsub/js/jobsub.js
  7. 1 1
      apps/metastore/src/metastore/templates/metastore.mako
  8. 4 4
      apps/oozie/src/oozie/static/oozie/js/workflow.modal.js
  9. 2 2
      apps/oozie/src/oozie/templates/editor/create_workflow.mako
  10. 2 2
      apps/oozie/src/oozie/templates/editor/edit_workflow.mako
  11. 1 1
      apps/oozie/src/oozie/templates/editor2/submit_job_popup.mako
  12. 2 59
      apps/oozie/src/oozie/templates/utils.inc.mako
  13. 1 1
      apps/security/src/security/templates/hive.mako
  14. 2 2
      apps/security/src/security/templates/sentry.mako
  15. 9 8
      desktop/core/src/desktop/js/api/apiHelper.js
  16. 10 1
      desktop/core/src/desktop/js/hue.js
  17. 1000 0
      desktop/core/src/desktop/js/ko/bindings/ko.aceEditor.js
  18. 110 0
      desktop/core/src/desktop/js/ko/bindings/ko.aceResizer.js
  19. 34 0
      desktop/core/src/desktop/js/ko/bindings/ko.appAwareTemplateContextMenu.js
  20. 261 0
      desktop/core/src/desktop/js/ko/bindings/ko.assistVerticalResizer.js
  21. 24 0
      desktop/core/src/desktop/js/ko/bindings/ko.attachViewModelToElementData.js
  22. 47 0
      desktop/core/src/desktop/js/ko/bindings/ko.augmentHtml.js
  23. 249 0
      desktop/core/src/desktop/js/ko/bindings/ko.autocomplete.js
  24. 69 0
      desktop/core/src/desktop/js/ko/bindings/ko.autogrowInput.js
  25. 39 0
      desktop/core/src/desktop/js/ko/bindings/ko.blurHide.js
  26. 55 0
      desktop/core/src/desktop/js/ko/bindings/ko.bytesize.js
  27. 42 0
      desktop/core/src/desktop/js/ko/bindings/ko.chosen.js
  28. 95 0
      desktop/core/src/desktop/js/ko/bindings/ko.clearable.js
  29. 30 0
      desktop/core/src/desktop/js/ko/bindings/ko.clickForAceFocus.js
  30. 32 0
      desktop/core/src/desktop/js/ko/bindings/ko.clickToCopy.js
  31. 84 0
      desktop/core/src/desktop/js/ko/bindings/ko.codemirror.js
  32. 144 0
      desktop/core/src/desktop/js/ko/bindings/ko.contextMenu.js
  33. 64 0
      desktop/core/src/desktop/js/ko/bindings/ko.contextSubMenu.js
  34. 98 0
      desktop/core/src/desktop/js/ko/bindings/ko.datalist.js
  35. 539 0
      desktop/core/src/desktop/js/ko/bindings/ko.dateRangePicker.js
  36. 56 0
      desktop/core/src/desktop/js/ko/bindings/ko.datepicker.js
  37. 28 0
      desktop/core/src/desktop/js/ko/bindings/ko.dblClick.js
  38. 49 0
      desktop/core/src/desktop/js/ko/bindings/ko.delayedOverflow.js
  39. 88 0
      desktop/core/src/desktop/js/ko/bindings/ko.dockable.js
  40. 125 0
      desktop/core/src/desktop/js/ko/bindings/ko.documentChooser.js
  41. 86 0
      desktop/core/src/desktop/js/ko/bindings/ko.documentContextPopover.js
  42. 83 0
      desktop/core/src/desktop/js/ko/bindings/ko.draggableText.js
  43. 24 0
      desktop/core/src/desktop/js/ko/bindings/ko.dropdown.js
  44. 115 0
      desktop/core/src/desktop/js/ko/bindings/ko.dropzone.js
  45. 75 0
      desktop/core/src/desktop/js/ko/bindings/ko.duration.js
  46. 33 0
      desktop/core/src/desktop/js/ko/bindings/ko.ellipsis.js
  47. 40 0
      desktop/core/src/desktop/js/ko/bindings/ko.fadeVisible.js
  48. 43 0
      desktop/core/src/desktop/js/ko/bindings/ko.fetchMore.js
  49. 99 0
      desktop/core/src/desktop/js/ko/bindings/ko.fileChooser.js
  50. 526 0
      desktop/core/src/desktop/js/ko/bindings/ko.foreachVisible.js
  51. 134 0
      desktop/core/src/desktop/js/ko/bindings/ko.fresherEditor.js
  52. 29 0
      desktop/core/src/desktop/js/ko/bindings/ko.hdfsAutocomplete.js
  53. 38 0
      desktop/core/src/desktop/js/ko/bindings/ko.hdfsTree.js
  54. 139 0
      desktop/core/src/desktop/js/ko/bindings/ko.highlight.js
  55. 101 0
      desktop/core/src/desktop/js/ko/bindings/ko.hiveChooser.js
  56. 46 0
      desktop/core/src/desktop/js/ko/bindings/ko.html.js
  57. 49 0
      desktop/core/src/desktop/js/ko/bindings/ko.hueCheckAll.js
  58. 37 0
      desktop/core/src/desktop/js/ko/bindings/ko.hueCheckbox.js
  59. 44 0
      desktop/core/src/desktop/js/ko/bindings/ko.hueChecked.js
  60. 63 0
      desktop/core/src/desktop/js/ko/bindings/ko.hueLink.js
  61. 84 0
      desktop/core/src/desktop/js/ko/bindings/ko.hueSpinner.js
  62. 182 0
      desktop/core/src/desktop/js/ko/bindings/ko.hueach.js
  63. 48 0
      desktop/core/src/desktop/js/ko/bindings/ko.impalaDagre.js
  64. 24 0
      desktop/core/src/desktop/js/ko/bindings/ko.jHueRowSelector.js
  65. 61 0
      desktop/core/src/desktop/js/ko/bindings/ko.logResizer.js
  66. 78 0
      desktop/core/src/desktop/js/ko/bindings/ko.logScroller.js
  67. 50 0
      desktop/core/src/desktop/js/ko/bindings/ko.medium.js
  68. 42 0
      desktop/core/src/desktop/js/ko/bindings/ko.moment.js
  69. 41 0
      desktop/core/src/desktop/js/ko/bindings/ko.momentFromNow.js
  70. 64 0
      desktop/core/src/desktop/js/ko/bindings/ko.multiCheck.js
  71. 66 0
      desktop/core/src/desktop/js/ko/bindings/ko.multiCheckForeachVisible.js
  72. 63 0
      desktop/core/src/desktop/js/ko/bindings/ko.multiClick.js
  73. 46 0
      desktop/core/src/desktop/js/ko/bindings/ko.multiLineEllipsis.js
  74. 62 0
      desktop/core/src/desktop/js/ko/bindings/ko.numberFormat.js
  75. 39 0
      desktop/core/src/desktop/js/ko/bindings/ko.numericTextInput.js
  76. 41 0
      desktop/core/src/desktop/js/ko/bindings/ko.onClickOutside.js
  77. 34 0
      desktop/core/src/desktop/js/ko/bindings/ko.oneClickSelect.js
  78. 64 0
      desktop/core/src/desktop/js/ko/bindings/ko.parseArguments.js
  79. 28 0
      desktop/core/src/desktop/js/ko/bindings/ko.plotly.js
  80. 43 0
      desktop/core/src/desktop/js/ko/bindings/ko.publish.js
  81. 52 0
      desktop/core/src/desktop/js/ko/bindings/ko.readOnlyAce.js
  82. 33 0
      desktop/core/src/desktop/js/ko/bindings/ko.resizable.js
  83. 228 0
      desktop/core/src/desktop/js/ko/bindings/ko.select2.js
  84. 55 0
      desktop/core/src/desktop/js/ko/bindings/ko.simplesize.js
  85. 32 0
      desktop/core/src/desktop/js/ko/bindings/ko.slideVisible.js
  86. 62 0
      desktop/core/src/desktop/js/ko/bindings/ko.slider.js
  87. 54 0
      desktop/core/src/desktop/js/ko/bindings/ko.solrChooser.js
  88. 40 0
      desktop/core/src/desktop/js/ko/bindings/ko.spinEdit.js
  89. 187 0
      desktop/core/src/desktop/js/ko/bindings/ko.splitDraggable.js
  90. 95 0
      desktop/core/src/desktop/js/ko/bindings/ko.splitFlexDraggable.js
  91. 82 0
      desktop/core/src/desktop/js/ko/bindings/ko.sqlContextPopover.js
  92. 81 0
      desktop/core/src/desktop/js/ko/bindings/ko.storageContextPopover.js
  93. 50 0
      desktop/core/src/desktop/js/ko/bindings/ko.stretchDown.js
  94. 261 0
      desktop/core/src/desktop/js/ko/bindings/ko.tagEditor.js
  95. 35 0
      desktop/core/src/desktop/js/ko/bindings/ko.tagsNotAllowed.js
  96. 97 0
      desktop/core/src/desktop/js/ko/bindings/ko.templateContextMenu.js
  97. 209 0
      desktop/core/src/desktop/js/ko/bindings/ko.templatePopover.js
  98. 35 0
      desktop/core/src/desktop/js/ko/bindings/ko.textSqueezer.js
  99. 29 0
      desktop/core/src/desktop/js/ko/bindings/ko.timepicker.js
  100. 43 0
      desktop/core/src/desktop/js/ko/bindings/ko.toggle.js

+ 4 - 3
.eslintrc.js

@@ -6,11 +6,12 @@ const hueGlobals = [
   // global_js_constants.mako
   'IS_HUE_4', 'AUTOCOMPLETE_TIMEOUT','CACHEABLE_TTL','CSRF_TOKEN','HAS_MULTI_CLUSTER',
   'DROPZONE_HOME_DIR', 'ENABLE_SQL_SYNTAX_CHECK', 'HAS_NAVIGATOR', 'HAS_OPTIMIZER', 'HAS_WORKLOAD_ANALYTICS',
-  'HUE_CONTAINER', 'IS_EMBEDDED', 'IS_K8S_ONLY', 'HUE_VERSION', 'IS_NEW_INDEXER_ENABLED',
-  'IS_S3_ENABLED', 'DOCUMENT_TYPES', 'LOGGED_USERNAME', 'USER_HOME_DIR', 'LOGGED_USERGROUPS', 'METASTORE_PARTITION_LIMIT',
+  'HUE_CONTAINER', 'IS_EMBEDDED', 'isIE11', 'IS_K8S_ONLY', 'HUE_VERSION', 'IS_NEW_INDEXER_ENABLED','HUE_I18n',
+  'IS_S3_ENABLED', 'KO_DATERANGEPICKER_LABELS', 'DOCUMENT_TYPES', 'LOGGED_USERNAME', 'USER_HOME_DIR',
+  'LOGGED_USERGROUPS', 'METASTORE_PARTITION_LIMIT', 'WorkerGlobalScope',
 
   // other misc, TODO
-  'huePubSub', 'ApiHelper', 'SqlUtils', 'trackOnGA', 'ContextCatalog', 'DataCatalog'
+  'ace', 'CodeMirror', 'Dropzone', 'impalaDagre', 'less', 'MediumEditor', 'moment', 'Plotly', 'Role', 'sqlStatementsParser', 'trackOnGA'
 ];
 
 const globals = normalGlobals.concat(hueGlobals).reduce((acc, key) => {

+ 0 - 18
apps/beeswax/src/beeswax/static/beeswax/js/beeswax.vm.js

@@ -978,24 +978,6 @@ function showSection(section) {
 }
 
 
-// File browser button
-function getFileBrowseButton(inputElement) {
-  return $("<button>").addClass("btn").addClass("fileChooserBtn").text("..").click(function (e) {
-    e.preventDefault();
-    $("#filechooser").jHueFileChooser({
-      initialPath: inputElement.val(),
-      onFileChoose: function (filePath) {
-        inputElement.val(filePath);
-        inputElement.trigger("change");
-        $("#chooseFile").modal("hide");
-      },
-      selectFolder: false,
-      createFolder: false
-    });
-    $("#chooseFile").modal("show");
-  });
-}
-
 function folderChooser(inputElement) {
   $("#folderchooser").jHueFileChooser({
     initialPath: inputElement.val(),

+ 2 - 2
apps/beeswax/src/beeswax/templates/execute.mako

@@ -1794,7 +1794,7 @@ var editables = function() {
     emptytext: "${ _('Empty description') }"
   });
 
-  $(".fileChooser:not(:has(~ button))").after(getFileBrowseButton($(".fileChooser:not(:has(~ button))")));
+  $(".fileChooser:not(:has(~ button))").after(hueUtils.getFileBrowseButton($(".fileChooser:not(:has(~ button))")));
 };
 
 $(document).one('fetched.design', editables);
@@ -2871,7 +2871,7 @@ $('.left-panel').on('mousewheel', function(e){
 % endif
 viewModel.design.fileResources.values.subscribe(function() {
   // File chooser button for file resources.
-  $(".fileChooser:not(:has(~ button))").after(getFileBrowseButton($(".fileChooser:not(:has(~ button))")));
+  $(".fileChooser:not(:has(~ button))").after(hueUtils.getFileBrowseButton($(".fileChooser:not(:has(~ button))")));
 });
 
 % if action == 'watch-results':

+ 1 - 23
apps/beeswax/src/beeswax/templates/save_results.mako

@@ -76,29 +76,7 @@ ${layout.menubar(section='query')}
 
 <script type="text/javascript">
   $(document).ready(function () {
-    $("input[name='target_dir']").after(getFileBrowseButton($("input[name='target_dir']")));
-
-    function getFileBrowseButton(inputElement) {
-      return $("<a>").addClass("btn").addClass("fileChooserBtn").text("..").click(function (e) {
-        e.preventDefault();
-        $("#fileChooserModal").jHueFileChooser({
-          onFolderChange:function (filePath) {
-            inputElement.val(filePath);
-          },
-          onFolderChoose:function (filePath) {
-            inputElement.val(filePath);
-            $("#fileChooserModal").slideUp();
-          },
-          createFolder:false,
-          uploadFile:false,
-          selectFolder:true,
-          initialPath:$.trim(inputElement.val())
-        });
-        $("#fileChooserModal").slideDown();
-        $("input[name='target_dir']").parents(".control-group").removeClass("error");
-        $("input[name='target_dir']").parents(".control-group").find(".fileChooserBtn").removeClass("btn-danger");
-      });
-    }
+    $("input[name='target_dir']").after(hueUtils.getFileBrowseButton($("input[name='target_dir']")));
 
     var viewModel = {
       toWhere: ko.observable("${ extract_field_data(form['save_target']) }")

+ 1 - 21
apps/beeswax/src/beeswax/templates/watch_results.mako

@@ -425,27 +425,7 @@ $(document).ready(function () {
   });
 
 
-  $("input[name='target_dir']").after(getFileBrowseButton($("input[name='target_dir']")));
-
-  function getFileBrowseButton(inputElement) {
-    return $("<a>").addClass("btn").addClass("fileChooserBtn").addClass("hide").text("..").click(function (e) {
-      e.preventDefault();
-      $("#fileChooserModal").jHueFileChooser({
-        onFolderChange:function (filePath) {
-          inputElement.val(filePath);
-        },
-        onFolderChoose:function (filePath) {
-          inputElement.val(filePath);
-          $("#fileChooserModal").slideUp();
-        },
-        createFolder:false,
-        uploadFile:false,
-        selectFolder:true,
-        initialPath:$.trim(inputElement.val())
-      });
-      $("#fileChooserModal").slideDown();
-    });
-  }
+  $("input[name='target_dir']").after(hueUtils.getFileBrowseButton($("input[name='target_dir']")));
 
   $("#collapse").click(function () {
     $(".sidebar-nav").parent().css("margin-left", "-31%");

+ 3 - 48
apps/jobsub/src/jobsub/static/jobsub/js/jobsub.js

@@ -143,71 +143,26 @@ function showSection(section) {
   addFileBrowseButton();
 }
 
-function getFileBrowseButton(inputElement, folderSelectable, fileSelectable, uploadFile, createFolder) {
-  var modal = null;
-  if (fileSelectable && folderSelectable) {
-    modal = "#choosePath";
-  } else if (folderSelectable) {
-    modal = "#chooseDirectory";
-  } else {
-    modal = "#chooseFile";
-  }
-  var chooser = modal + " .chooser";
-
-  return $("<button>").addClass("btn").addClass("chooserBtn").text("..").click(function(e){
-    e.preventDefault();
-
-    $(chooser).jHueFileChooser({
-      initialPath: inputElement.val(),
-      onFileChoose: function(filePath) {
-        if (fileSelectable){
-          inputElement.val(filePath);
-          inputElement.change();
-          addFileBrowseButton();
-          $(modal).modal("hide");
-        }
-      },
-      onFolderChange: function(filePath) {
-        inputElement.val(filePath);
-        inputElement.change();
-      },
-      onFolderChoose: function(filePath) {
-        if (folderSelectable){
-          inputElement.val(filePath);
-          inputElement.change();
-          addFileBrowseButton();
-          $(modal).modal("hide");
-        }
-      },
-      createFolder: createFolder,
-      selectFolder: folderSelectable,
-      uploadFile: uploadFile,
-      forceRefresh: true
-    });
-    $(modal).modal("show");
-  })
-}
-
 function addFileBrowseButton() {
   // Filechooser.
   $(".pathChooserKo").each(function(){
     var self = $(this);
     if (!self.siblings().hasClass('chooserBtn')) {
-      self.after(getFileBrowseButton(self, true, true, false, true));
+      self.after(hueUtils.getFileBrowseButton(self, true, true, false, true));
     }
   });
 
   $(".pathFileChooserKo").each(function(){
     var self = $(this);
     if (!self.siblings().hasClass('chooserBtn')) {
-      self.after(getFileBrowseButton(self, false, true, true, false));
+      self.after(hueUtils.getFileBrowseButton(self, false, true, true, false));
     }
   });
 
   $(".pathFolderChooserKo").each(function(){
     var self = $(this);
     if (!self.siblings().hasClass('chooserBtn')) {
-      self.after(getFileBrowseButton(self, true, false, false, true));
+      self.after(hueUtils.getFileBrowseButton(self, true, false, false, true));
     }
   });
 }

+ 1 - 1
apps/metastore/src/metastore/templates/metastore.mako

@@ -121,7 +121,7 @@ ${ components.menubar(is_embeddable) }
     <!-- ko if: editingTable -->
       <!-- ko with: table -->
       <li class="editable-breadcrumb-input">
-        <input type="text" data-bind="hivechooser: { data: catalogEntry.name, database: $parent.catalogEntry.name, namespace: $parent.catalogEntry.namespace, compute: $parent.catalogEntry.compute, skipColumns: true, searchEverywhere: true, onChange: function(val) { $parent.setTableByName(val); $parent.editingTable(false); }, apiHelperUser: '${ user }', apiHelperType: $root.source().type }" autocomplete="off" />
+        <input type="text" data-bind="hiveChooser: { data: catalogEntry.name, database: $parent.catalogEntry.name, namespace: $parent.catalogEntry.namespace, compute: $parent.catalogEntry.compute, skipColumns: true, searchEverywhere: true, onChange: function(val) { $parent.setTableByName(val); $parent.editingTable(false); }, apiHelperUser: '${ user }', apiHelperType: $root.source().type }" autocomplete="off" />
       </li>
       <!-- /ko -->
     <!-- /ko -->

+ 4 - 4
apps/oozie/src/oozie/static/oozie/js/workflow.modal.js

@@ -93,10 +93,10 @@ var ModalModule = function($, ko) {
   module.prototype.addDecorations = function () {
     $(".popover").remove();
 
-    $("input[name='job_xml']:not(.pathChooser)").addClass("pathChooser").after(getFileBrowseButton($("input[name='job_xml']:not(.pathChooser)")));
-    $("input[name='jar_path']").addClass("pathChooser").after(getFileBrowseButton($("input[name='jar_path']")));
-    $("input[name='script_path']").addClass("pathChooser").after(getFileBrowseButton($("input[name='script_path']")));
-    $("input[name='command']").addClass("pathChooser").after(getFileBrowseButton($("input[name='command']")));
+    $("input[name='job_xml']:not(.pathChooser)").addClass("pathChooser").after(hueUtils.getFileBrowseButton($("input[name='job_xml']:not(.pathChooser)")));
+    $("input[name='jar_path']").addClass("pathChooser").after(hueUtils.getFileBrowseButton($("input[name='jar_path']")));
+    $("input[name='script_path']").addClass("pathChooser").after(hueUtils.getFileBrowseButton($("input[name='script_path']")));
+    $("input[name='command']").addClass("pathChooser").after(hueUtils.getFileBrowseButton($("input[name='command']")));
 
     if (typeof CodeMirror !== 'undefined' && $("textarea[name='xml']").length > 0) {
       $("textarea[name='xml']").hide();

+ 2 - 2
apps/oozie/src/oozie/templates/editor/create_workflow.mako

@@ -88,8 +88,8 @@ ${ utils.path_chooser_libs(True, True) }
 
 <script>
   $(document).ready(function(){
-    $("input[name='deployment_dir']").after(getFileBrowseButton($("input[name='deployment_dir']"), true));
-    $("input[name='job_xml']").after(getFileBrowseButton($("input[name='job_xml']"), false));
+    $("input[name='deployment_dir']").after(hueUtils.getFileBrowseButton($("input[name='deployment_dir']"), true));
+    $("input[name='job_xml']").after(hueUtils.getFileBrowseButton($("input[name='job_xml']"), false));
   });
 </script>
 

+ 2 - 2
apps/oozie/src/oozie/templates/editor/edit_workflow.mako

@@ -948,7 +948,7 @@ $('#importOozieAction').on('click', '.action-row', function(e) {
 ko.bindingHandlers.fileChooser = {
   init: function(element, valueAccessor, allBindings, model) {
     var self = $(element);
-    self.after(getFileBrowseButton(self, true));
+    self.after(hueUtils.getFileBrowseButton(self, true));
   }
 };
 
@@ -1140,7 +1140,7 @@ ${ utils.path_chooser_libs(True) }
 <script>
   $(document).ready(function(){
     $("input[name='job_xml']").next().remove();
-    $("input[name='job_xml']").after(getFileBrowseButton($("input[name='job_xml']"), false));
+    $("input[name='job_xml']").after(hueUtils.getFileBrowseButton($("input[name='job_xml']"), false));
   });
 </script>
 

+ 1 - 1
apps/oozie/src/oozie/templates/editor2/submit_job_popup.mako

@@ -131,7 +131,7 @@
 
 <script type="text/javascript">
   $('.submit-form .filechooser-input').each(function(){
-    $(this).after(getFileBrowseButton($(this), true, null, true));
+    $(this).after(hueUtils.getFileBrowseButton($(this), true, null, true));
   });
 
   $(".now-link").on("click", function(){

+ 2 - 59
apps/oozie/src/oozie/templates/utils.inc.mako

@@ -346,70 +346,13 @@
         $(".pathChooser").each(function(){
           var self = $(this);
           % if select_folder:
-              self.after(getFileBrowseButton(self, true));
+              self.after(hueUtils.getFileBrowseButton(self, true));
           % else:
-              self.after(getFileBrowseButton(self));
+              self.after(hueUtils.getFileBrowseButton(self));
           % endif
         });
       % endif
     });
-
-    function getFileBrowseButton(inputElement, selectFolder) {
-      return $("<button>").addClass("btn").addClass("fileChooserBtn").text("..").click(function (e) {
-        e.preventDefault();
-        // check if it's a relative path
-        var pathAddition = "";
-        if ($.trim(inputElement.val()) != "") {
-          var checkPath = "/filebrowser/view=${ workflow.deployment_dir }" + "/" + inputElement.val();
-          $.getJSON(checkPath, function (data) {
-            pathAddition = "${ workflow.deployment_dir }/";
-            callFileChooser();
-          }).fail(function () {
-            callFileChooser();
-          });
-        }
-        else {
-          callFileChooser();
-        }
-
-        function callFileChooser() {
-          $("#fileChooserModal").jHueFileChooser({
-            selectFolder:(selectFolder) ? true : false,
-            onFolderChoose:function (filePath) {
-              handleChoice(filePath);
-              if (selectFolder) {
-                $("#chooseFileModal").modal("hide");
-              }
-            },
-            onFileChoose:function (filePath) {
-              handleChoice(filePath);
-              $("#chooseFileModal").modal("hide");
-            },
-            createFolder:false,
-            uploadFile:true,
-            initialPath:$.trim(inputElement.val()) != "" ? pathAddition + inputElement.val() : "${ workflow.deployment_dir }",
-            errorRedirectPath:"",
-            forceRefresh:true
-          });
-          $("#chooseFileModal").modal("show");
-        }
-
-        function handleChoice(filePath) {
-          var _deployDir = $.trim("${ workflow.deployment_dir }");
-          if (_deployDir != "" && filePath.indexOf(_deployDir) > -1) {
-            filePath = filePath.substring(_deployDir.length + 1);
-            if (filePath == "") {
-              filePath = "./";
-            }
-            if (filePath.indexOf("//") == 0){
-              filePath = filePath.substr(1);
-            }
-          }
-          inputElement.val(filePath);
-          inputElement.change();
-        }
-      });
-    }
   </script>
 </%def>
 

+ 1 - 1
apps/security/src/security/templates/hive.mako

@@ -62,7 +62,7 @@ ${ layout.menubar(section='hive1', is_embeddable=is_embeddable) }
         <i class="fa fa-fw fa-1halfx muted" data-bind="css: {'fa-circle-o': privilegeType() != 'db' , 'fa-check-circle-o': privilegeType() == 'db'}"></i>
       </a>
     </div>
-    <input type="text" data-bind="hivechooser: $data.path, enable: privilegeType() == 'db', apiHelperUser: '${ user }', apiHelperType: 'hive'" placeholder="dbName.tableName <CTRL+SPACE>">
+    <input type="text" data-bind="hiveChooser: $data.path, enable: privilegeType() == 'db', apiHelperUser: '${ user }', apiHelperType: 'hive'" placeholder="dbName.tableName <CTRL+SPACE>">
 
     <div class="inline-block" style="vertical-align: middle">
       <a class="pointer" style="padding-top: 4px" data-bind="click: function(){ privilegeType('uri'); action('ALL'); }">

+ 2 - 2
apps/security/src/security/templates/sentry.mako

@@ -65,7 +65,7 @@ ${ layout.menubar(section=component, is_embeddable=is_embeddable) }
           <i class="fa fa-fw fa-1halfx muted" data-bind="css: {'fa-circle-o': privilegeType() != 'DATABASE', 'fa-check-circle-o': privilegeType() == 'DATABASE'}"></i>
         </a>
       </div>
-      <input type="text" data-bind="hivechooser: $data.path, enable: privilegeType() == 'DATABASE', apiHelperUser: '${ user }', apiHelperType: 'hive'" placeholder="dbName.tableName <CTRL+SPACE>">
+      <input type="text" data-bind="hiveChooser: $data.path, enable: privilegeType() == 'DATABASE', apiHelperUser: '${ user }', apiHelperType: 'hive'" placeholder="dbName.tableName <CTRL+SPACE>">
 
       <div class="inline-block" style="vertical-align: middle">
         <a class="pointer" style="padding-top: 4px" data-bind="click: function(){ privilegeType('URI'); action('ALL'); }">
@@ -79,7 +79,7 @@ ${ layout.menubar(section=component, is_embeddable=is_embeddable) }
     <!-- /ko -->
 
     <!-- ko if: $root.component() == 'solr' -->
-      <input type="text" class="input-xxlarge" data-bind="solrchooser: $data.path" placeholder="collection or config name <CTRL+SPACE>">
+      <input type="text" class="input-xxlarge" data-bind="solrChooser: $data.path" placeholder="collection or config name <CTRL+SPACE>">
       <select data-bind="options: privilegeType() == 'CONFIG' ? $root.availableSolrConfigActions : $root.availableActions(authorizables()), value: $data.action, enable: privilegeType() != 'CONFIG'" style="width: 100px; margin-bottom: 0"></select>
     <!-- /ko -->
 

+ 9 - 8
desktop/core/src/desktop/js/api/apiHelper.js

@@ -1170,11 +1170,9 @@ class ApiHelper {
 
   /**
    * @param {Object} options
-   * @param {Function} options.successCallback
-   * @param {Function} [options.errorCallback]
+   * @param {number} options.uuid
    * @param {boolean} [options.silenceErrors]
    * @param {boolean} [options.fetchContents]
-   * @param {number} options.uuid
    *
    * @return {CancellablePromise}
    */
@@ -1198,9 +1196,12 @@ class ApiHelper {
           );
         }
       }
-    }).fail(errorResponse => {
-      deferred.reject(self.assistErrorHandler(errorResponse));
-    });
+    }).fail(
+      self.assistErrorCallback({
+        silenceErrors: options.silenceErrors,
+        errorCallback: deferred.reject
+      })
+    );
     return new CancellablePromise(deferred, request);
   }
 
@@ -2248,8 +2249,8 @@ class ApiHelper {
     const request = self.simplePost(
       url,
       {
-        'cluster': JSON.stringify(options.cluster),
-        'query_id': '"' + options.queryId + '"'
+        cluster: JSON.stringify(options.cluster),
+        query_id: '"' + options.queryId + '"'
       },
       {
         silenceErrors: options.silenceErrors,

+ 10 - 1
desktop/core/src/desktop/js/hue.js

@@ -21,8 +21,10 @@ import filesize from 'filesize';
 import qq from 'ext/fileuploader.custom';
 import page from 'page';
 import localforage from 'localforage';
+import sprintf from 'sprintf-js';
 
 import ko from 'knockout';
+import 'ko/ko.all';
 import komapping from 'knockout.mapping';
 import 'ext/ko.editable.custom';
 import 'ext/ko.selectize.custom';
@@ -39,13 +41,17 @@ import hueDebug from 'utils/hueDebug';
 import hueDrop from 'utils/hueDrop';
 import huePubSub from 'utils/huePubSub';
 import hueUtils from 'utils/hueUtils';
+import MultiLineEllipsisHandler from 'utils/multiLineEllipsisHandler';
 
+import AceLocationHandler from 'sql/aceLocationHandler';
 import sqlUtils from 'sql/sqlUtils';
+import sqlWorkerHandler from 'sql/sqlWorkerHandler';
 
 import 'assist/assistViewModel';
 
 // TODO: Migrate away
 window._ = _;
+window.AceLocationHandler = AceLocationHandler;
 window.apiHelper = apiHelper;
 window.CancellablePromise = CancellablePromise;
 window.contextCatalog = contextCatalog;
@@ -59,6 +65,9 @@ window.hueDrop = hueDrop;
 window.ko = ko;
 window.ko.mapping = komapping;
 window.localforage = localforage;
+window.MultiLineEllipsisHandler = MultiLineEllipsisHandler;
 window.page = page;
+window.sprintf = sprintf;
 window.sqlUtils = sqlUtils;
-window.qq = qq;
+window.sqlWorkerHandler = sqlWorkerHandler;
+window.qq = qq;

+ 1000 - 0
desktop/core/src/desktop/js/ko/bindings/ko.aceEditor.js

@@ -0,0 +1,1000 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import apiHelper from 'api/apiHelper';
+import AceLocationHandler from 'sql/aceLocationHandler';
+import huePubSub from 'utils/huePubSub';
+import sqlUtils from 'sql/sqlUtils';
+
+// TODO: Depends on Ace
+
+ko.bindingHandlers.aceEditor = {
+  init: function(element, valueAccessor) {
+    const $el = $(element);
+    const options = ko.unwrap(valueAccessor());
+    const snippet = options.snippet;
+    const aceOptions = options.aceOptions || {};
+
+    const disposeFunctions = [];
+
+    const dispose = function() {
+      disposeFunctions.forEach(dispose => {
+        dispose();
+      });
+    };
+
+    ko.utils.domNodeDisposal.addDisposeCallback(element, dispose);
+
+    $el.text(snippet.statement_raw());
+
+    const editor = ace.edit($el.attr('id'));
+    const AceRange = ace.require('ace/range').Range;
+
+    const resizeAce = function() {
+      window.setTimeout(() => {
+        try {
+          editor.resize(true);
+        } catch (e) {
+          // Can happen when the editor hasn't been initialized
+        }
+      }, 0);
+    };
+
+    const assistToggleSub = huePubSub.subscribe('assist.set.manual.visibility', resizeAce);
+    const resizePubSub = huePubSub.subscribe('split.panel.resized', resizeAce);
+    disposeFunctions.push(() => {
+      assistToggleSub.remove();
+      resizePubSub.remove();
+    });
+
+    const aceLocationHandler = new AceLocationHandler({
+      editor: editor,
+      editorId: $el.attr('id'),
+      snippet: snippet,
+      i18n: { expandStar: options.expandStar, contextTooltip: options.contextTooltip }
+    });
+    disposeFunctions.push(() => {
+      aceLocationHandler.dispose();
+    });
+
+    editor.session.setMode(snippet.getAceMode());
+    editor.setOptions({
+      fontSize: apiHelper.getFromTotalStorage(
+        'hue.ace',
+        'fontSize',
+        navigator.platform && navigator.platform.toLowerCase().indexOf('linux') > -1
+          ? '14px'
+          : '12px'
+      )
+    });
+
+    function processErrorsAndWarnings(type, list) {
+      editor.clearErrorsAndWarnings(type);
+      let offset = 0;
+      if (snippet.isSqlDialect() && editor.getSelectedText()) {
+        const selectionRange = editor.getSelectionRange();
+        offset = Math.min(selectionRange.start.row, selectionRange.end.row);
+      }
+      if (list.length > 0) {
+        list.forEach((item, cnt) => {
+          if (item.line !== null) {
+            if (type === 'error') {
+              editor.addError(item.message, item.line + offset);
+            } else {
+              editor.addWarning(item.message, item.line + offset);
+            }
+            if (cnt === 0) {
+              editor.scrollToLine(item.line + offset, true, true, () => {});
+              if (item.col !== null) {
+                editor.renderer.scrollCursorIntoView(
+                  { row: item.line + offset, column: item.col + 10 },
+                  0.5
+                );
+              }
+            }
+          }
+        });
+      }
+    }
+
+    const errorsSub = snippet.errors.subscribe(newErrors => {
+      processErrorsAndWarnings('error', newErrors);
+    });
+
+    const aceWarningsSub = snippet.aceWarnings.subscribe(newWarnings => {
+      processErrorsAndWarnings('warning', newWarnings);
+    });
+
+    const aceErrorsSub = snippet.aceErrors.subscribe(newErrors => {
+      processErrorsAndWarnings('error', newErrors);
+    });
+
+    disposeFunctions.push(() => {
+      errorsSub.dispose();
+      aceWarningsSub.dispose();
+      aceErrorsSub.dispose();
+    });
+
+    let darkThemeEnabled = apiHelper.getFromTotalStorage('ace', 'dark.theme.enabled', false);
+    editor.setTheme(darkThemeEnabled ? 'ace/theme/hue_dark' : 'ace/theme/hue');
+
+    const editorOptions = {
+      enableSnippets: true,
+      showGutter: false,
+      showLineNumbers: false,
+      showPrintMargin: false,
+      scrollPastEnd: 0.1,
+      minLines: 1,
+      maxLines: 25
+    };
+
+    editor.enabledMenuOptions = {
+      setShowInvisibles: true,
+      setTabSize: true,
+      setShowGutter: true
+    };
+
+    editor.customMenuOptions = {
+      setEnableDarkTheme: function(enabled) {
+        darkThemeEnabled = enabled;
+        apiHelper.setInTotalStorage('ace', 'dark.theme.enabled', darkThemeEnabled);
+        editor.setTheme(darkThemeEnabled ? 'ace/theme/hue_dark' : 'ace/theme/hue');
+      },
+      getEnableDarkTheme: function() {
+        return darkThemeEnabled;
+      },
+      setEnableAutocompleter: function(enabled) {
+        editor.setOption('enableBasicAutocompletion', enabled);
+        apiHelper.setInTotalStorage('hue.ace', 'enableBasicAutocompletion', enabled);
+        const $enableLiveAutocompletionChecked = $('#setEnableLiveAutocompletion:checked');
+        const $setEnableLiveAutocompletion = $('#setEnableLiveAutocompletion');
+        if (enabled && $enableLiveAutocompletionChecked.length === 0) {
+          $setEnableLiveAutocompletion.trigger('click');
+        } else if (!enabled && $enableLiveAutocompletionChecked.length !== 0) {
+          $setEnableLiveAutocompletion.trigger('click');
+        }
+      },
+      getEnableAutocompleter: function() {
+        return editor.getOption('enableBasicAutocompletion');
+      },
+      setEnableLiveAutocompletion: function(enabled) {
+        editor.setOption('enableLiveAutocompletion', enabled);
+        apiHelper.setInTotalStorage('hue.ace', 'enableLiveAutocompletion', enabled);
+        if (enabled && $('#setEnableAutocompleter:checked').length === 0) {
+          $('#setEnableAutocompleter').trigger('click');
+        }
+      },
+      getEnableLiveAutocompletion: function() {
+        return editor.getOption('enableLiveAutocompletion');
+      },
+      setFontSize: function(size) {
+        if (size.toLowerCase().indexOf('px') === -1 && size.toLowerCase().indexOf('em') === -1) {
+          size += 'px';
+        }
+        editor.setOption('fontSize', size);
+        apiHelper.setInTotalStorage('hue.ace', 'fontSize', size);
+      },
+      getFontSize: function() {
+        let size = editor.getOption('fontSize');
+        if (size.toLowerCase().indexOf('px') === -1 && size.toLowerCase().indexOf('em') === -1) {
+          size += 'px';
+        }
+        return size;
+      }
+    };
+
+    if (window.ENABLE_SQL_SYNTAX_CHECK && window.Worker) {
+      let errorHighlightingEnabled = apiHelper.getFromTotalStorage(
+        'hue.ace',
+        'errorHighlightingEnabled',
+        true
+      );
+
+      if (errorHighlightingEnabled) {
+        aceLocationHandler.attachSqlSyntaxWorker();
+      }
+
+      editor.customMenuOptions.setErrorHighlighting = function(enabled) {
+        errorHighlightingEnabled = enabled;
+        apiHelper.setInTotalStorage('hue.ace', 'errorHighlightingEnabled', enabled);
+        if (enabled) {
+          aceLocationHandler.attachSqlSyntaxWorker();
+        } else {
+          aceLocationHandler.detachSqlSyntaxWorker();
+        }
+      };
+      editor.customMenuOptions.getErrorHighlighting = function() {
+        return errorHighlightingEnabled;
+      };
+      editor.customMenuOptions.setClearIgnoredSyntaxChecks = function() {
+        apiHelper.setInTotalStorage('hue.syntax.checker', 'suppressedRules', {});
+        $('#setClearIgnoredSyntaxChecks')
+          .hide()
+          .before('<div style="margin-top:5px;float:right;">done</div>');
+      };
+      editor.customMenuOptions.getClearIgnoredSyntaxChecks = function() {
+        return false;
+      };
+    }
+
+    $.extend(editorOptions, aceOptions);
+
+    editorOptions['enableBasicAutocompletion'] = apiHelper.getFromTotalStorage(
+      'hue.ace',
+      'enableBasicAutocompletion',
+      true
+    );
+    if (editorOptions['enableBasicAutocompletion']) {
+      editorOptions['enableLiveAutocompletion'] = apiHelper.getFromTotalStorage(
+        'hue.ace',
+        'enableLiveAutocompletion',
+        true
+      );
+    }
+
+    editor.setOptions(editorOptions);
+
+    const AceAutocomplete = ace.require('ace/autocomplete').Autocomplete;
+
+    if (!editor.completer) {
+      editor.completer = new AceAutocomplete();
+    }
+    editor.completer.exactMatch = !snippet.isSqlDialect();
+
+    const initAutocompleters = function() {
+      if (editor.completers) {
+        editor.completers.length = 0;
+        if (snippet.type() === 'hive' || snippet.type() === 'impala') {
+          if (options.useNewAutocompleter) {
+            editor.useHueAutocompleter = true;
+          } else {
+            editor.completers.push(snippet.autocompleter);
+          }
+        } else {
+          editor.completers.push(langTools.snippetCompleter);
+          editor.completers.push(langTools.textCompleter);
+          editor.completers.push(langTools.keyWordCompleter);
+          editor.completers.push(snippet.autocompleter);
+        }
+      }
+    };
+
+    const langTools = ace.require('ace/ext/language_tools');
+    langTools.textCompleter.setSqlMode(snippet.isSqlDialect());
+
+    initAutocompleters();
+
+    const UNICODES_TO_REMOVE = /[\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u200B\u202F\u205F\u3000\uFEFF]/gi; //taken from https://www.cs.tut.fi/~jkorpela/chars/spaces.html
+
+    const removeUnicodes = function(value) {
+      return value.replace(UNICODES_TO_REMOVE, ' ');
+    };
+
+    let placeHolderElement = null;
+    let placeHolderVisible = false;
+    const placeHolderText = snippet.getPlaceHolder();
+    if (placeHolderText) {
+      placeHolderElement = $('<div>')
+        .text(placeHolderText)
+        .css('margin-left', '6px')
+        .addClass('ace_invisible ace_emptyMessage');
+      if (editor.getValue().length === 0) {
+        placeHolderElement.appendTo(editor.renderer.scroller);
+        placeHolderVisible = true;
+      }
+    }
+
+    const pasteListener = editor.on('paste', e => {
+      e.text = removeUnicodes(e.text);
+    });
+
+    disposeFunctions.push(() => {
+      editor.off('paste', pasteListener);
+    });
+
+    const inputListener = editor.on('input', () => {
+      if (editor.getValue().length === 0) {
+        if (!placeHolderVisible && placeHolderElement) {
+          placeHolderElement.appendTo(editor.renderer.scroller);
+          placeHolderVisible = true;
+        }
+      } else {
+        placeHolderElement.remove();
+        placeHolderVisible = false;
+      }
+      if (options.updateOnInput) {
+        snippet.statement_raw(removeUnicodes(editor.getValue()));
+      }
+    });
+
+    disposeFunctions.push(() => {
+      editor.off('input', inputListener);
+    });
+
+    if (snippet.aceCursorPosition()) {
+      editor.moveCursorToPosition(snippet.aceCursorPosition());
+      window.setTimeout(() => {
+        editor.centerSelection();
+      }, 0);
+    }
+
+    const focusListener = editor.on('focus', () => {
+      initAutocompleters();
+      snippet.inFocus(true);
+      $('.ace-editor').data('last-active-editor', false);
+      $el.data('last-active-editor', true);
+      if (editor.session.$backMarkers) {
+        for (const marker in editor.session.$backMarkers) {
+          if (editor.session.$backMarkers[marker].clazz === 'highlighted') {
+            editor.session.removeMarker(editor.session.$backMarkers[marker].id);
+          }
+        }
+      }
+    });
+
+    disposeFunctions.push(() => {
+      editor.off('focus', focusListener);
+    });
+
+    const changeSelectionListener = editor.selection.on('changeSelection', () => {
+      snippet.selectedStatement(editor.getSelectedText());
+    });
+
+    disposeFunctions.push(() => {
+      editor.selection.off('changeSelection', changeSelectionListener);
+    });
+
+    const blurListener = editor.on('blur', () => {
+      snippet.inFocus(false);
+      snippet.statement_raw(removeUnicodes(editor.getValue()));
+      if (options.onBlur) {
+        options.onBlur($el, removeUnicodes(editor.getValue()));
+      }
+    });
+
+    disposeFunctions.push(() => {
+      editor.off('blur', blurListener);
+    });
+
+    editor.previousSize = 0;
+
+    // TODO: Get rid of this
+    const idInterval = window.setInterval(() => {
+      editor.session.getMode().$id = snippet.getAceMode(); // forces the id again because of Ace command internals
+    }, 100);
+
+    disposeFunctions.push(() => {
+      window.clearInterval(idInterval);
+    });
+
+    editor.middleClick = false;
+    const mousedownListener = editor.on('mousedown', e => {
+      if (e.domEvent.which === 2) {
+        // middle click
+        editor.middleClick = true;
+        const tempText = editor.getSelectedText();
+        if (e.$pos) {
+          editor.session.insert(e.$pos, tempText);
+        }
+        window.setTimeout(() => {
+          editor.middleClick = false;
+          if (e.$pos) {
+            editor.moveCursorTo(e.$pos.row, e.$pos.column + tempText.length);
+          }
+        }, 200);
+      }
+    });
+
+    disposeFunctions.push(() => {
+      editor.off('mousedown', mousedownListener);
+    });
+
+    const aceReplaceSub = huePubSub.subscribe('ace.replace', data => {
+      const Range = ace.require('ace/range').Range;
+      const range = new Range(
+        data.location.first_line - 1,
+        data.location.first_column - 1,
+        data.location.last_line - 1,
+        data.location.last_column - 1
+      );
+      editor
+        .getSession()
+        .getDocument()
+        .replace(range, data.text);
+    });
+
+    disposeFunctions.push(() => {
+      aceReplaceSub.remove();
+    });
+
+    const clickListener = editor.on('click', () => {
+      editor.clearErrorsAndWarnings();
+    });
+
+    disposeFunctions.push(() => {
+      editor.off('click', clickListener);
+    });
+
+    const changeListener = editor.on('change', () => {
+      snippet.statement_raw(removeUnicodes(editor.getValue()));
+      editor.session.getMode().$id = snippet.getAceMode();
+      const currentSize = editor.session.getLength();
+      if (
+        currentSize !== editor.previousSize &&
+        currentSize >= editorOptions.minLines &&
+        currentSize <= editorOptions.maxLines
+      ) {
+        editor.previousSize = editor.session.getLength();
+        $(document).trigger('editorSizeChanged');
+      }
+      // automagically change snippet type
+      const firstLine = editor.session.getLine(0);
+      if (firstLine.indexOf('%') === 0 && firstLine.charAt(firstLine.length - 1) === ' ') {
+        const availableSnippets = snippet.availableSnippets;
+        let removeFirstLine = false;
+        for (let i = 0; i < availableSnippets.length; i++) {
+          if ($.trim(firstLine.substr(1)) === availableSnippets[i].type()) {
+            snippet.type(availableSnippets[i].type());
+            removeFirstLine = true;
+            break;
+          }
+        }
+        if (removeFirstLine) {
+          editor.session.remove(new AceRange(0, 0, 0, 200));
+        }
+      }
+    });
+
+    disposeFunctions.push(() => {
+      editor.off('change', changeListener);
+    });
+
+    editor.commands.addCommand({
+      name: 'execute',
+      bindKey: { win: 'Ctrl-Enter', mac: 'Command-Enter|Ctrl-Enter' },
+      exec: function() {
+        snippet.statement_raw(removeUnicodes(editor.getValue()));
+        snippet.execute();
+      }
+    });
+
+    editor.commands.addCommand({
+      name: 'switchTheme',
+      bindKey: { win: 'Ctrl-Alt-t', mac: 'Command-Alt-t' },
+      exec: function() {
+        darkThemeEnabled = !darkThemeEnabled;
+        apiHelper.setInTotalStorage('ace', 'dark.theme.enabled', darkThemeEnabled);
+        editor.setTheme(darkThemeEnabled ? 'ace/theme/hue_dark' : 'ace/theme/hue');
+      }
+    });
+
+    editor.commands.addCommand({
+      name: 'new',
+      bindKey: { win: 'Ctrl-e', mac: 'Command-e' },
+      exec: function() {
+        huePubSub.publish('editor.create.new');
+      }
+    });
+
+    editor.commands.addCommand({
+      name: 'save',
+      bindKey: { win: 'Ctrl-s', mac: 'Command-s|Ctrl-s' },
+      exec: function() {
+        huePubSub.publish('editor.save');
+      }
+    });
+
+    editor.commands.addCommand({
+      name: 'esc',
+      bindKey: { win: 'Ctrl-Shift-p', mac: 'Ctrl-Shift-p|Command-Shift-p' },
+      exec: function() {
+        huePubSub.publish('editor.presentation.toggle');
+      }
+    });
+
+    editor.commands.bindKey('Ctrl-P', 'golineup');
+
+    editor.commands.addCommand({
+      name: 'format',
+      bindKey: {
+        win: 'Ctrl-i|Ctrl-Shift-f|Ctrl-Alt-l',
+        mac: 'Command-i|Ctrl-i|Ctrl-Shift-f|Command-Shift-f|Ctrl-Shift-l|Cmd-Shift-l'
+      },
+      exec: function() {
+        if (
+          [
+            'ace/mode/hive',
+            'ace/mode/impala',
+            'ace/mode/sql',
+            'ace/mode/mysql',
+            'ace/mode/pgsql',
+            'ace/mode/sqlite',
+            'ace/mode/oracle'
+          ].indexOf(snippet.getAceMode()) > -1
+        ) {
+          $.post(
+            '/notebook/api/format',
+            {
+              statements:
+                editor.getSelectedText() !== '' ? editor.getSelectedText() : editor.getValue()
+            },
+            data => {
+              if (data.status === 0) {
+                if (editor.getSelectedText() !== '') {
+                  editor.session.replace(
+                    editor.session.selection.getRange(),
+                    data.formatted_statements
+                  );
+                } else {
+                  editor.setValue(data.formatted_statements);
+                  snippet.statement_raw(removeUnicodes(editor.getValue()));
+                }
+              }
+            }
+          );
+        }
+      }
+    });
+
+    editor.commands.addCommand({
+      name: 'gotolinealternative',
+      bindKey: { win: 'Ctrl-j', mac: 'Command-j|Ctrl-j' },
+      exec: editor.commands.commands['gotoline'].exec
+    });
+
+    const isNewStatement = function() {
+      return /^\s*$/.test(editor.getValue()) || /^.*;\s*$/.test(editor.getTextBeforeCursor());
+    };
+
+    const insertSqlAtCursor = function(text, cursorEndAdjust, menu) {
+      const before = editor.getTextBeforeCursor();
+      if (/\S+$/.test(before)) {
+        text = ' ' + text;
+      }
+      if (menu) {
+        menu.hide();
+      }
+      editor.session.insert(editor.getCursorPosition(), text);
+      if (cursorEndAdjust !== 0) {
+        const cursor = editor.getCursorPosition();
+        editor.moveCursorToPosition({ row: cursor.row, column: cursor.column + cursorEndAdjust });
+      }
+      editor.clearSelection();
+      editor.focus();
+    };
+
+    const insertTableAtCursorSub = huePubSub.subscribe('editor.insert.table.at.cursor', details => {
+      if ($el.data('last-active-editor')) {
+        const qualifiedName =
+          snippet.database() === details.database
+            ? details.name
+            : details.database + '.' + details.name;
+        if (isNewStatement()) {
+          insertSqlAtCursor('SELECT * FROM ' + qualifiedName + ' LIMIT 100;', -1);
+        } else {
+          insertSqlAtCursor(qualifiedName + ' ', 0);
+        }
+      }
+    });
+
+    const insertColumnAtCursorSub = huePubSub.subscribe(
+      'editor.insert.column.at.cursor',
+      details => {
+        if ($el.data('last-active-editor')) {
+          if (isNewStatement()) {
+            const qualifiedFromName =
+              snippet.database() === details.database
+                ? details.table
+                : details.database + '.' + details.table;
+            insertSqlAtCursor(
+              'SELECT ' + details.name + ' FROM ' + qualifiedFromName + ' LIMIT 100;',
+              -1
+            );
+          } else {
+            insertSqlAtCursor(details.name + ' ', 0);
+          }
+        }
+      }
+    );
+
+    const insertAtCursorSub = huePubSub.subscribe('editor.insert.at.cursor', text => {
+      if ($el.data('last-active-editor')) {
+        insertSqlAtCursor(text + ' ', 0);
+      }
+    });
+
+    disposeFunctions.push(() => {
+      insertTableAtCursorSub.remove();
+      insertColumnAtCursorSub.remove();
+      insertAtCursorSub.remove();
+    });
+
+    const dblClickHdfsItemSub = huePubSub.subscribe('assist.dblClickHdfsItem', assistHdfsEntry => {
+      if ($el.data('last-active-editor')) {
+        editor.session.insert(editor.getCursorPosition(), "'" + assistHdfsEntry.path + "'");
+      }
+    });
+
+    disposeFunctions.push(() => {
+      dblClickHdfsItemSub.remove();
+    });
+
+    const dblClickAdlsItemSub = huePubSub.subscribe('assist.dblClickAdlsItem', assistHdfsEntry => {
+      if ($el.data('last-active-editor')) {
+        editor.session.insert(editor.getCursorPosition(), 'adl:/' + assistHdfsEntry.path + "'");
+      }
+    });
+
+    disposeFunctions.push(() => {
+      dblClickAdlsItemSub.remove();
+    });
+
+    const dblClickGitItemSub = huePubSub.subscribe('assist.dblClickGitItem', assistGitEntry => {
+      if ($el.data('last-active-editor')) {
+        editor.session.setValue(assistGitEntry.fileContent());
+      }
+    });
+
+    disposeFunctions.push(() => {
+      dblClickGitItemSub.remove();
+    });
+
+    const dblClickS3ItemSub = huePubSub.subscribe('assist.dblClickS3Item', assistS3Entry => {
+      if ($el.data('last-active-editor')) {
+        editor.session.insert(editor.getCursorPosition(), "'S3A://" + assistS3Entry.path + "'");
+      }
+    });
+
+    disposeFunctions.push(() => {
+      dblClickS3ItemSub.remove();
+    });
+
+    const sampleErrorInsertSub = huePubSub.subscribe('sample.error.insert.click', popoverEntry => {
+      const table = popoverEntry.identifierChain[popoverEntry.identifierChain.length - 1]['name'];
+      const text = 'SELECT * FROM ' + table + ' LIMIT 100;';
+      insertSqlAtCursor(text, -1);
+    });
+
+    disposeFunctions.push(() => {
+      sampleErrorInsertSub.remove();
+    });
+
+    const $tableDropMenu = $el.next('.table-drop-menu');
+    const $identifierDropMenu = $tableDropMenu.find('.editor-drop-identifier');
+
+    const hideDropMenu = function() {
+      $tableDropMenu.css('opacity', 0);
+      window.setTimeout(() => {
+        $tableDropMenu.hide();
+      }, 300);
+    };
+
+    const documentClickListener = function(event) {
+      if ($tableDropMenu.find($(event.target)).length === 0) {
+        hideDropMenu();
+      }
+    };
+
+    $(document).on('click', documentClickListener);
+
+    disposeFunctions.push(() => {
+      $(document).off('click', documentClickListener);
+    });
+
+    let lastMeta = {};
+    const draggableTextSub = huePubSub.subscribe('draggable.text.meta', meta => {
+      lastMeta = meta;
+      if (meta.isView) {
+        $tableDropMenu.find('.editor-drop-update').hide();
+        $tableDropMenu.find('.editor-drop-insert').hide();
+        $tableDropMenu.find('.editor-drop-drop').hide();
+        $tableDropMenu.find('.editor-drop-view').show();
+      } else {
+        $tableDropMenu.find('.editor-drop-update').show();
+        $tableDropMenu.find('.editor-drop-insert').show();
+        $tableDropMenu.find('.editor-drop-drop').show();
+        $tableDropMenu.find('.editor-drop-view').hide();
+      }
+      if (
+        typeof meta !== 'undefined' &&
+        typeof meta.database !== 'undefined' &&
+        typeof meta.table !== 'undefined'
+      ) {
+        $identifierDropMenu.text(meta.database + '.' + meta.table);
+      }
+    });
+
+    disposeFunctions.push(() => {
+      draggableTextSub.remove();
+    });
+
+    const menu = ko.bindingHandlers.contextMenu.initContextMenu(
+      $tableDropMenu,
+      $('.content-panel')
+    );
+
+    $tableDropMenu.find('.editor-drop-value').click(() => {
+      insertSqlAtCursor(
+        sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.database) +
+          '.' +
+          sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.table) +
+          ' ',
+        0,
+        menu
+      );
+    });
+
+    $tableDropMenu.find('.editor-drop-select').click(() => {
+      insertSqlAtCursor(
+        'SELECT * FROM ' +
+          sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.database) +
+          '.' +
+          sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.table) +
+          ' LIMIT 100;',
+        -1,
+        menu
+      );
+      $tableDropMenu.hide();
+    });
+
+    $tableDropMenu.find('.editor-drop-insert').click(() => {
+      insertSqlAtCursor(
+        'INSERT INTO ' +
+          sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.database) +
+          '.' +
+          sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.table) +
+          ' VALUES ();',
+        -2,
+        menu
+      );
+    });
+
+    $tableDropMenu.find('.editor-drop-update').click(() => {
+      insertSqlAtCursor(
+        'UPDATE ' +
+          sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.database) +
+          '.' +
+          sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.table) +
+          ' SET ',
+        0,
+        menu
+      );
+    });
+
+    $tableDropMenu.find('.editor-drop-view').click(() => {
+      insertSqlAtCursor(
+        'DROP VIEW ' +
+          sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.database) +
+          '.' +
+          sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.table) +
+          ';',
+        -1,
+        menu
+      );
+    });
+
+    $tableDropMenu.find('.editor-drop-drop').click(() => {
+      insertSqlAtCursor(
+        'DROP TABLE ' +
+          sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.database) +
+          '.' +
+          sqlUtils.backTickIfNeeded(lastMeta.type, lastMeta.table) +
+          ';',
+        -1,
+        menu
+      );
+    });
+
+    $el.droppable({
+      accept: '.draggableText',
+      drop: function(e, ui) {
+        const position = editor.renderer.screenToTextCoordinates(e.clientX, e.clientY);
+        let text = ui.helper.text();
+        if (lastMeta.type === 's3' || lastMeta.type === 'hdfs' || lastMeta.type === 'adls') {
+          text = "'" + lastMeta.definition.path + "'";
+        }
+        editor.moveCursorToPosition(position);
+        const before = editor.getTextBeforeCursor();
+        if (lastMeta.database && lastMeta.table && !lastMeta.column && /.*;|^\s*$/.test(before)) {
+          menu.show(e);
+        } else {
+          if (/\S+$/.test(before) && before.charAt(before.length - 1) !== '.') {
+            text = ' ' + text;
+          }
+          const after = editor.getTextAfterCursor();
+          if (after.length > 0 && after.charAt(0) !== ' ' && text.charAt(text.length - 1) !== ' ') {
+            text += ' ';
+          }
+          editor.session.insert(position, text);
+          position.column += text.length;
+          editor.clearSelection();
+        }
+      }
+    });
+
+    let autocompleteTemporarilyDisabled = false;
+    let autocompleteThrottle = -1;
+    const afterExecListener = editor.commands.on('afterExec', e => {
+      if (editor.getOption('enableLiveAutocompletion') && e.command.name === 'insertstring') {
+        if (/\S+\(\)$/.test(e.args)) {
+          editor.moveCursorTo(
+            editor.getCursorPosition().row,
+            editor.getCursorPosition().column - 1
+          );
+          return;
+        }
+        window.clearTimeout(autocompleteThrottle);
+        autocompleteThrottle = window.setTimeout(() => {
+          const textBeforeCursor = editor.getTextBeforeCursor();
+          let questionMarkMatch;
+          if ($('.hue-ace-autocompleter').length > 0) {
+            questionMarkMatch = textBeforeCursor.match(/select\s+(\? from \S+[^.]\s$)/i);
+          } else {
+            questionMarkMatch = textBeforeCursor.match(/select\s+(\? from \S+[^.]$)/i);
+          }
+          if (questionMarkMatch && $('.ace_autocomplete:visible').length === 0) {
+            editor.moveCursorTo(
+              editor.getCursorPosition().row,
+              editor.getCursorPosition().column - (questionMarkMatch[1].length - 1)
+            );
+            editor.removeTextBeforeCursor(1);
+            huePubSub.publish('editor.refresh.statement.locations', snippet);
+            window.setTimeout(() => {
+              editor.execCommand('startAutocomplete');
+            }, 1);
+          } else if (/\.$/.test(textBeforeCursor)) {
+            huePubSub.publish('editor.refresh.statement.locations', snippet);
+            window.setTimeout(() => {
+              editor.execCommand('startAutocomplete');
+            }, 1);
+          }
+        }, 400);
+      }
+      editor.session.getMode().$id = snippet.getAceMode(); // forces the id again because of Ace command internals
+      // if it's pig and before it's LOAD ' we disable the autocomplete and show a filechooser btn
+      if (editor.session.getMode().$id === 'ace/mode/pig' && e.args) {
+        const textBefore = editor.getTextBeforeCursor();
+        if (
+          (e.args === "'" &&
+            textBefore.toUpperCase().indexOf('LOAD ') > -1 &&
+            textBefore.toUpperCase().indexOf('LOAD ') === textBefore.toUpperCase().length - 5) ||
+          (textBefore.toUpperCase().indexOf("LOAD '") > -1 &&
+            textBefore.toUpperCase().indexOf("LOAD '") === textBefore.toUpperCase().length - 6)
+        ) {
+          if (editor.getOption('enableBasicAutocompletion')) {
+            editor.disableAutocomplete();
+            autocompleteTemporarilyDisabled = true;
+          }
+          const btn = editor.showFileButton();
+          btn.on('click', ie => {
+            ie.preventDefault();
+            // TODO: Turn the ace file chooser into a component and remove css class references
+            const $aceFileChooseContent = $('.ace-filechooser-content');
+            if (!$aceFileChooseContent.data('jHueFileChooser')) {
+              if ($aceFileChooseContent.data('spinner') == null) {
+                $aceFileChooseContent.data('spinner', $aceFileChooseContent.html());
+              } else {
+                $aceFileChooseContent.html($aceFileChooseContent.data('spinner'));
+              }
+              $aceFileChooseContent.jHueFileChooser({
+                onFileChoose: function(filePath) {
+                  editor.session.insert(editor.getCursorPosition(), filePath + "'");
+                  editor.hideFileButton();
+                  if (autocompleteTemporarilyDisabled) {
+                    editor.enableAutocomplete();
+                    autocompleteTemporarilyDisabled = false;
+                  }
+                  $('.ace-filechooser').hide();
+                },
+                selectFolder: false,
+                createFolder: false
+              });
+            }
+            $('.ace-filechooser')
+              .css({
+                top: $(ie.currentTarget).position().top,
+                left: $(ie.currentTarget).position().left
+              })
+              .show();
+          });
+        } else {
+          editor.hideFileButton();
+          if (autocompleteTemporarilyDisabled) {
+            editor.enableAutocomplete();
+            autocompleteTemporarilyDisabled = false;
+          }
+        }
+        if (
+          e.args !== "'" &&
+          textBefore.toUpperCase().indexOf("LOAD '") > -1 &&
+          textBefore.toUpperCase().indexOf("LOAD '") === textBefore.toUpperCase().length - 6
+        ) {
+          editor.hideFileButton();
+          if (autocompleteTemporarilyDisabled) {
+            editor.enableAutocomplete();
+            autocompleteTemporarilyDisabled = false;
+          }
+        }
+      }
+    });
+
+    disposeFunctions.push(() => {
+      editor.commands.off('afterExec', afterExecListener);
+    });
+    editor.$blockScrolling = Infinity;
+    snippet.ace(editor);
+  },
+
+  update: function(element, valueAccessor) {
+    const options = ko.unwrap(valueAccessor());
+    const snippet = options.snippet;
+    const AceRange = ace.require('ace/range').Range;
+    if (snippet.ace()) {
+      const editor = snippet.ace();
+      if (typeof options.readOnly !== 'undefined') {
+        editor.setReadOnly(options.readOnly);
+      }
+      const range = options.highlightedRange ? options.highlightedRange() : null;
+      editor.session.setMode(snippet.getAceMode());
+      if (range && JSON.stringify(range.start) !== JSON.stringify(range.end)) {
+        let conflictingWithErrorMarkers = false;
+        if (editor.session.$backMarkers) {
+          for (const marker in editor.session.$backMarkers) {
+            if (editor.session.$backMarkers[marker].clazz === 'ace_error-line') {
+              const errorRange = editor.session.$backMarkers[marker].range;
+              if (range.start.row <= errorRange.end.row && range.end.row >= errorRange.start.row) {
+                conflictingWithErrorMarkers = true;
+              }
+            }
+            if (editor.session.$backMarkers[marker].clazz === 'highlighted') {
+              editor.session.removeMarker(editor.session.$backMarkers[marker].id);
+            }
+          }
+        }
+        if (!conflictingWithErrorMarkers) {
+          const lineOffset = snippet.lastAceSelectionRowOffset();
+          window.setTimeout(() => {
+            editor.session.addMarker(
+              new AceRange(
+                range.start.row + lineOffset,
+                range.start.column,
+                range.end.row + lineOffset,
+                range.end.column
+              ),
+              'highlighted',
+              'line'
+            );
+            ace
+              .require('ace/lib/dom')
+              .importCssString(
+                '.highlighted {\
+                  background-color: #E3F7FF;\
+                  position: absolute;\
+              }'
+              );
+            editor.scrollToLine(range.start.row + lineOffset, true, true, () => {});
+          }, 0);
+        }
+      }
+      try {
+        editor._emit('change');
+      } catch (e) {}
+    }
+  }
+};

+ 110 - 0
desktop/core/src/desktop/js/ko/bindings/ko.aceResizer.js

@@ -0,0 +1,110 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import huePubSub from 'utils/huePubSub';
+
+ko.bindingHandlers.aceResizer = {
+  init: function(element, valueAccessor) {
+    const options = ko.unwrap(valueAccessor());
+    const ace = options.snippet.ace;
+    const $target = $(options.target);
+    const $resizer = $(element);
+    const $contentPanel = $('.content-panel');
+    const $execStatus = $resizer.prev('.snippet-execution-status');
+    const $variables = $resizer.siblings('.snippet-row').find('.variables');
+
+    let lastEditorSize = $.totalStorage('hue.editor.editor.size') || 131;
+    let editorHeight = Math.floor(lastEditorSize / 16);
+    $target.height(lastEditorSize);
+    let autoExpand =
+      typeof options.snippet.aceAutoExpand !== 'undefined' ? options.snippet.aceAutoExpand : true;
+    let draggedOnce = false;
+
+    function throttleChange() {
+      if (autoExpand && !draggedOnce) {
+        const maxAutoLines = Math.floor(($(window).height() - 80) / 2 / 16);
+        let resized = false;
+        if (ace().session.getLength() > editorHeight) {
+          if (ace().session.getLength() < maxAutoLines) {
+            $target.height(ace().session.getLength() * 16);
+          } else {
+            $target.height(maxAutoLines * 16); // height of maxAutoLines
+          }
+          resized = true;
+        } else {
+          $target.height(
+            Math.max(
+              ace().session.getLength() * 16,
+              $.totalStorage('hue.editor.editor.size') || 131
+            )
+          );
+          resized = true;
+        }
+        if (ace().session.getLength() === editorHeight) {
+          resized = false;
+        }
+        if (resized && $target.height() !== lastEditorSize) {
+          ace().resize();
+          editorHeight = Math.min(maxAutoLines, ace().session.getLength());
+          lastEditorSize = $target.height();
+          huePubSub.publish('redraw.fixed.headers');
+        }
+      }
+    }
+
+    let changeTimeout = -1;
+    ace().on('change', () => {
+      window.clearTimeout(changeTimeout);
+      changeTimeout = window.setTimeout(throttleChange, 10);
+    });
+
+    const setAutoExpandSubscription = huePubSub.subscribe('ace.set.autoexpand', options => {
+      if (ace().container.id === options.snippet.id()) {
+        autoExpand = options.autoExpand;
+      }
+    });
+
+    $resizer.draggable({
+      axis: 'y',
+      start: options.onStart ? options.onStart : function() {},
+      drag: function(event, ui) {
+        draggedOnce = true;
+        const currentHeight =
+          ui.offset.top +
+          $contentPanel.scrollTop() -
+          (125 + $execStatus.outerHeight(true) + $variables.outerHeight(true));
+        $target.css('height', currentHeight + 'px');
+        ace().resize();
+        ui.offset.top = 0;
+        ui.position.top = 0;
+      },
+      stop: function(event, ui) {
+        ui.offset.top = 0;
+        ui.position.top = 0;
+        $.totalStorage('hue.editor.editor.size', $target.height());
+        huePubSub.publish('redraw.fixed.headers');
+        $(document).trigger('editorSizeChanged');
+      }
+    });
+
+    ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
+      setAutoExpandSubscription.remove();
+    });
+  }
+};

+ 34 - 0
desktop/core/src/desktop/js/ko/bindings/ko.appAwareTemplateContextMenu.js

@@ -0,0 +1,34 @@
+// 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 huePubSub from 'utils/huePubSub';
+
+ko.bindingHandlers.appAwareTemplateContextMenu = {
+  init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
+    viewModel.$currentApp = ko.observable('');
+    huePubSub.subscribe('set.current.app.name', viewModel.$currentApp);
+    huePubSub.publish('get.current.app.name');
+    ko.bindingHandlers.templateContextMenu.init(
+      element,
+      valueAccessor,
+      allBindings,
+      viewModel,
+      bindingContext
+    );
+  }
+};

+ 261 - 0
desktop/core/src/desktop/js/ko/bindings/ko.assistVerticalResizer.js

@@ -0,0 +1,261 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import apiHelper from 'api/apiHelper';
+import huePubSub from 'utils/huePubSub';
+
+ko.bindingHandlers.assistVerticalResizer = {
+  init: function(element, valueAccessor) {
+    const $container = $(element);
+    const options = ko.unwrap(valueAccessor());
+    const panelDefinitions = options.panels;
+
+    const checkForElements = function() {
+      const $allPanels = $container.children('.assist-inner-panel');
+      const $allExtras = $container.children('.assist-fixed-height');
+      if (
+        panelDefinitions().length === $allPanels.length &&
+        ($allExtras.length > 0 || options.noFixedHeights)
+      ) {
+        ko.bindingHandlers.assistVerticalResizer.updateWhenRendered(element, valueAccessor);
+      } else {
+        timeout = window.setTimeout(checkForElements, 10);
+      }
+    };
+
+    checkForElements();
+    let timeout = -1;
+    panelDefinitions.subscribe(() => {
+      window.clearTimeout(timeout);
+      timeout = window.setTimeout(checkForElements, 10);
+    });
+  },
+
+  updateWhenRendered: function(element, valueAccessor) {
+    const options = ko.unwrap(valueAccessor());
+    const panelDefinitions = options.panels;
+
+    const $container = $(element);
+    let $allPanels = $container.children('.assist-inner-panel');
+    const $allResizers = $container.children('.assist-resizer');
+    const $allExtras = $container.children('.assist-fixed-height');
+
+    let allExtrasHeight = 0;
+    $allExtras.each((idx, extra) => {
+      allExtrasHeight += $(extra).outerHeight(true);
+    });
+
+    window.clearInterval($container.data('height_interval'));
+
+    if (panelDefinitions().length === 0) {
+      $allExtras.show();
+      return;
+    }
+    if (panelDefinitions().length === 1) {
+      const adjustHeightSingle = function() {
+        $allPanels.height($container.innerHeight() - allExtrasHeight);
+      };
+
+      const heightAdjustInterval = window.setInterval(adjustHeightSingle, 800);
+      adjustHeightSingle();
+      $container.data('height_interval', heightAdjustInterval);
+
+      $(window).resize(adjustHeightSingle);
+      huePubSub.subscribe('assist.forceRender', () => {
+        window.setTimeout(adjustHeightSingle, 200);
+      });
+      $allExtras.show();
+      $allPanels.show();
+      return;
+    }
+
+    const panelRatios = apiHelper.getFromTotalStorage('assist', 'innerPanelRatios', {});
+
+    let totalRatios = 0;
+    $.each($allPanels, (idx, panel) => {
+      const panelDef = panelDefinitions()[idx];
+      if (!panelRatios[panelDef.type]) {
+        panelRatios[panelDef.type] = 1 / panelDefinitions().length;
+      }
+      totalRatios += panelRatios[panelDef.type];
+      $(panel).data('minHeight', panelDef.minHeight);
+    });
+
+    // Normalize the ratios in case new panels were added or removed.
+    if (totalRatios !== 1) {
+      const diff = 1 / totalRatios;
+      $.each(panelDefinitions(), (idx, panel) => {
+        panelRatios[panel.type] = panelRatios[panel.type] * diff;
+      });
+    }
+
+    let totalHeight = -1;
+    let containerTop = $container.offset().top;
+
+    // Resizes all containers according to the set ratios
+    const resizeByRatio = function() {
+      if (totalHeight === $container.innerHeight()) {
+        return;
+      }
+      $allPanels = $container.children('.assist-inner-panel');
+      totalHeight = $container.innerHeight();
+      containerTop = $container.offset().top;
+
+      $.each($allPanels, (idx, panel) => {
+        const panelDef = panelDefinitions()[idx];
+        if (!panelRatios[panelDef.type] || $allPanels.length === 1) {
+          panelRatios[panelDef.type] = 1 / panelDefinitions().length;
+        }
+        totalRatios += panelRatios[panelDef.type];
+        $(panel).data('minHeight', panelDef.minHeight);
+      });
+
+      const availableForPanels = totalHeight - allExtrasHeight;
+      let leftoverSpace = 0;
+      $allPanels.each((idx, panel) => {
+        const $panel = $(panel);
+        const desiredHeight = availableForPanels * panelRatios[panelDefinitions()[idx].type];
+        const newHeight = Math.max($panel.data('minHeight'), desiredHeight);
+        $panel.height(newHeight);
+        leftoverSpace += newHeight - desiredHeight;
+      });
+      // The minheight is greater than the ratio so we shrink where possible
+      if (leftoverSpace > 0) {
+        $allPanels.each((idx, panel) => {
+          if (leftoverSpace === 0) {
+            return false;
+          }
+          const $panel = $(panel);
+          const currentHeight = $panel.height();
+          const possibleContribution = Math.min(
+            currentHeight - $panel.data('minHeight'),
+            leftoverSpace
+          );
+          if (possibleContribution > 0) {
+            $panel.height(currentHeight - possibleContribution);
+            leftoverSpace -= possibleContribution;
+          }
+        });
+      }
+    };
+
+    resizeByRatio();
+    $(window).resize(resizeByRatio);
+
+    window.setTimeout(resizeByRatio, 1000);
+    huePubSub.subscribe('assist.forceRender', () => {
+      window.setTimeout(resizeByRatio, 200);
+    });
+
+    $allExtras.show();
+    $allPanels.show();
+
+    const fitPanelHeights = function($panelsToResize, desiredTotalHeight) {
+      let currentHeightOfPanels = 0;
+
+      let noMoreSpace = true;
+      $panelsToResize.each((idx, panel) => {
+        const $panel = $(panel);
+        const panelHeight = $panel.outerHeight(true);
+        noMoreSpace = noMoreSpace && panelHeight <= $panel.data('minHeight');
+        currentHeightOfPanels += panelHeight;
+      });
+
+      let distanceToGo = desiredTotalHeight - currentHeightOfPanels;
+      if (noMoreSpace && distanceToGo < 0) {
+        return;
+      }
+
+      // Add all to the first panel if expanding (distanceToGo is positive
+      if (distanceToGo >= 0) {
+        $panelsToResize.first().height($panelsToResize.first().height() + distanceToGo + 'px');
+        return;
+      }
+
+      // Remove as much as possible on each panel if shrinking (distanceToGo is negative)
+      $panelsToResize.each((idx, panel) => {
+        const $panel = $(panel);
+        const initialHeight = $panel.height();
+        const newHeight = Math.max($panel.data('minHeight'), initialHeight + distanceToGo);
+        if (initialHeight === newHeight) {
+          return true;
+        }
+        $panel.height(newHeight);
+        distanceToGo += initialHeight - newHeight;
+        if (distanceToGo >= 0) {
+          return false;
+        }
+      });
+    };
+
+    $allResizers.each((idx, resizer) => {
+      const $resizer = $(resizer);
+      let extrasBeforeHeight = 0;
+      $resizer.prevAll('.assist-fixed-height').each((idx, extra) => {
+        extrasBeforeHeight += $(extra).outerHeight(true);
+      });
+      const $panelsBefore = $resizer.prevAll('.assist-inner-panel');
+      let limitBefore = extrasBeforeHeight;
+      $panelsBefore.each((idx, panel) => {
+        limitBefore += $(panel).data('minHeight');
+      });
+
+      const extrasAfterHeight = allExtrasHeight - extrasBeforeHeight;
+      const $panelsAfter = $resizer.nextAll('.assist-inner-panel');
+      let requiredSpaceAfter = extrasAfterHeight;
+      $panelsAfter.each((idx, panel) => {
+        requiredSpaceAfter += $(panel).data('minHeight');
+      });
+
+      $resizer.draggable({
+        axis: 'y',
+        drag: function(event, ui) {
+          const limitAfter = totalHeight - requiredSpaceAfter;
+          const position = ui.offset.top - containerTop;
+          if (position > limitBefore && position < limitAfter) {
+            fitPanelHeights($panelsBefore, position - extrasBeforeHeight);
+            fitPanelHeights($panelsAfter, totalHeight - extrasAfterHeight - position);
+          } else if (position > limitAfter) {
+            fitPanelHeights($panelsBefore, limitAfter - extrasBeforeHeight);
+            fitPanelHeights($panelsAfter, totalHeight - extrasAfterHeight - limitAfter);
+          } else if (position < limitBefore) {
+            fitPanelHeights($panelsBefore, limitBefore - extrasBeforeHeight);
+            fitPanelHeights($panelsAfter, totalHeight - extrasAfterHeight - limitBefore);
+          }
+
+          ui.offset.top = 0;
+          ui.position.top = 0;
+        },
+        stop: function(event, ui) {
+          ui.offset.top = 0;
+          ui.position.top = 0;
+          let totalHeightForPanels = 0;
+          $allPanels.each((idx, panel) => {
+            totalHeightForPanels += $(panel).outerHeight(true);
+          });
+          $allPanels.each((idx, panel) => {
+            panelRatios[panelDefinitions()[idx].type] =
+              $(panel).outerHeight(true) / totalHeightForPanels;
+          });
+          apiHelper.setInTotalStorage('assist', 'innerPanelRatios', panelRatios);
+        }
+      });
+    });
+  }
+};

+ 24 - 0
desktop/core/src/desktop/js/ko/bindings/ko.attachViewModelToElementData.js

@@ -0,0 +1,24 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.attachViewModelToElementData = {
+  init: function(el, valueAccessor, allBindingsAccessor, viewModel) {
+    $(el).data('__ko_vm', viewModel);
+  }
+};

+ 47 - 0
desktop/core/src/desktop/js/ko/bindings/ko.augmentHtml.js

@@ -0,0 +1,47 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+// TODO: less from bootstrap?
+
+ko.bindingHandlers.augmentHtml = {
+  render: function(element, valueAccessor) {
+    const _val = ko.unwrap(valueAccessor());
+    const _enc = $('<span>').html(_val);
+    if (_enc.find('style').length > 0) {
+      const parser = new less.Parser();
+      $(_enc.find('style')).each((cnt, item) => {
+        const _less = '.result-container {' + $(item).text() + '}';
+        try {
+          parser.parse(_less, (err, tree) => {
+            $(item).text(tree.toCSS());
+          });
+        } catch (e) {}
+      });
+      $(element).html(_enc.html());
+    } else {
+      $(element).html(_val);
+    }
+  },
+  init: function(element, valueAccessor, allBindingsAccessor, viewModel) {
+    ko.bindingHandlers.augmentHtml.render(element, valueAccessor, allBindingsAccessor, viewModel);
+  },
+  update: function(element, valueAccessor, allBindingsAccessor, viewModel) {
+    ko.bindingHandlers.augmentHtml.render(element, valueAccessor, allBindingsAccessor, viewModel);
+  }
+};

+ 249 - 0
desktop/core/src/desktop/js/ko/bindings/ko.autocomplete.js

@@ -0,0 +1,249 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import huePubSub from 'utils/huePubSub';
+
+ko.bindingHandlers.autocomplete = {
+  init: function(element, valueAccessor) {
+    let options = valueAccessor();
+    const $element = $(element);
+
+    const delay = 400;
+
+    const showSpinner = function() {
+      if (options.showSpinner) {
+        $element.addClass('input-spinner');
+      }
+    };
+
+    let spinThrottle = -1;
+    const hideSpinner = function() {
+      window.clearTimeout(spinThrottle);
+      $element.removeClass('input-spinner');
+    };
+
+    options = $.extend(
+      {
+        addCount: false,
+        closeOnEnter: true,
+        blurOnEnter: false,
+        classPrefix: 'hue-',
+        showOnFocus: false,
+        minLength: 0,
+        limitWidthToInput: false,
+        minWidth: 200,
+        disabled: true,
+        delay: delay,
+        search: function() {
+          window.clearTimeout(spinThrottle);
+          if (!$element.hueAutocomplete('option', 'disabled')) {
+            spinThrottle = window.setTimeout(showSpinner, 50);
+          }
+        },
+        open: function() {
+          hideSpinner();
+        },
+        close: function() {
+          hideSpinner();
+        }
+      },
+      options
+    );
+
+    if (options.addCount) {
+      const oldSource = options.source;
+      options.source = function(request, callback) {
+        oldSource(request, values => {
+          callback(values);
+          const $menu = $($element.data('custom-hueAutocomplete').menu.element);
+          $menu.children('.autocomplete-count').remove();
+          const count = options.realCountCallback
+            ? options.realCountCallback()
+            : values.filter(value => {
+                return !value.divider && !value.noMatch;
+              }).length;
+          if (count > 0) {
+            $('<div>')
+              .addClass('autocomplete-count')
+              .text('(' + count + ')')
+              .appendTo($menu);
+          }
+        });
+      };
+    }
+
+    if (typeof $().hueAutocomplete === 'undefined') {
+      $.widget('custom.hueAutocomplete', $.ui.autocomplete, {
+        _renderItemData: function(ul, item) {
+          if (item.error && this.options.errorTemplate) {
+            const $li = $(
+              '<li data-bind="template: { name: \'' + this.options.errorTemplate + '\' }">'
+            )
+              .addClass(this.options.classPrefix + 'autocomplete-item')
+              .appendTo(ul)
+              .data('ui-autocomplete-item', item);
+            ko.applyBindings(item, $li[0]);
+          } else if (item.noMatch && this.options.noMatchTemplate) {
+            const $li = $(
+              '<li data-bind="template: { name: \'' + this.options.noMatchTemplate + '\' }">'
+            )
+              .addClass(this.options.classPrefix + 'autocomplete-item')
+              .appendTo(ul)
+              .data('ui-autocomplete-item', item);
+            ko.applyBindings(item, $li[0]);
+          } else if (item.divider) {
+            $('<li/>')
+              .addClass(this.options.classPrefix + 'autocomplete-divider')
+              .appendTo(ul);
+          } else {
+            const $li = $(
+              '<li data-bind="template: { name: \'' +
+                this.options.itemTemplate +
+                '\', data: $data }">'
+            )
+              .addClass(this.options.classPrefix + 'autocomplete-item')
+              .appendTo(ul)
+              .data('ui-autocomplete-item', item);
+            ko.applyBindings(item.data, $li[0]);
+          }
+        },
+        _resizeMenu: function() {
+          // This overrides the default behaviour of using dropdown width of the same size as input autocomplete box
+          if (options.limitWidthToInput) {
+            this.menu.element.outerWidth(options.minWidth);
+          }
+        },
+        _renderMenu: function(ul, items) {
+          const self = this;
+          hideSpinner();
+          if (options.limitWidthToInput) {
+            ul.css('max-width', Math.max(options.minWidth, $element.outerWidth(true)) + 'px');
+          }
+
+          ul.addClass(this.options.classPrefix + 'autocomplete');
+          $.each(items, (index, item) => {
+            self._renderItemData(ul, item);
+          });
+        }
+      });
+    }
+
+    if (options.closeOnEnter || options.onEnter || options.blurOnEnter) {
+      $element.on('keyup', e => {
+        if (e.which === 13) {
+          if (options.reopenPattern && options.reopenPattern.test($element.val())) {
+            window.setTimeout(() => {
+              $element.hueAutocomplete('search', $element.val());
+            }, 0);
+            return;
+          }
+          if (options.closeOnEnter) {
+            hideSpinner();
+            $element.hueAutocomplete('close');
+            // Prevent autocomplete on enter
+            $element.hueAutocomplete('option', 'disabled', true);
+            window.setTimeout(() => {
+              $element.hueAutocomplete('option', 'disabled', false);
+            }, delay + 200);
+          }
+          if (options.valueObservable) {
+            options.valueObservable($element.val());
+          }
+          if (options.onEnter) {
+            options.onEnter();
+          }
+          if (options.blurOnEnter) {
+            hideSpinner();
+            $element.blur();
+          }
+        }
+      });
+    }
+
+    $element.on('keydown', e => {
+      // ctrl + backspace to delete words
+      if (e.which === 8 && e.ctrlKey) {
+        const lastSpaceOrColon = Math.max(
+          $element.val().lastIndexOf(' '),
+          $element.val().lastIndexOf(':')
+        );
+        if (lastSpaceOrColon < $element.val().length - 1) {
+          if (lastSpaceOrColon !== -1) {
+            $element.val($element.val().substring(0, lastSpaceOrColon + 1));
+          } else {
+            $element.val('');
+          }
+          e.preventDefault();
+          return false;
+        }
+      } else if (e.which === 32 && e.ctrlKey) {
+        $element.hueAutocomplete('search', $element.val());
+      }
+    });
+
+    if (options.showOnFocus) {
+      $element.on('focus', () => {
+        $element.hueAutocomplete('search', $element.val());
+      });
+    }
+
+    const closeSubscription = huePubSub.subscribe('autocomplete.close', () => {
+      hideSpinner();
+      $element.hueAutocomplete('close');
+    });
+
+    ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
+      closeSubscription.remove();
+    });
+
+    if (options.reopenPattern || options.valueObservable || options.onSelect) {
+      const oldSelect = options.select;
+      options.select = function(event, ui) {
+        if (options.reopenPattern && options.reopenPattern.test(ui.item.value)) {
+          window.setTimeout(() => {
+            $element.hueAutocomplete('search', $element.val());
+          }, 0);
+          return;
+        }
+        if (options.valueObservable) {
+          options.valueObservable(ui.item.value);
+        }
+        if (options.onSelect) {
+          options.onSelect(ui.item);
+        }
+        if (oldSelect) {
+          oldSelect(event, ui);
+        }
+      };
+    }
+
+    $element.hueAutocomplete(options);
+
+    const enableAutocomplete = function() {
+      if ($element.data('custom-hueAutocomplete')) {
+        $element.hueAutocomplete('option', 'disabled', false);
+        $element.off('click', enableAutocomplete);
+      } else {
+        window.setTimeout(enableAutocomplete, 200);
+      }
+    };
+    // IE 11 trick to prevent it from being shown on page refresh
+    $element.on('click', enableAutocomplete);
+  }
+};

+ 69 - 0
desktop/core/src/desktop/js/ko/bindings/ko.autogrowInput.js

@@ -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 $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.autogrowInput = {
+  init: function(element, valueAccessor) {
+    const o = $.extend(
+      {
+        minWidth: 0,
+        maxWidth: 1000,
+        comfortZone: 70
+      },
+      valueAccessor()
+    );
+
+    const minWidth = o.minWidth || $(element).width();
+    const input = $(element);
+    const testSubject = $('<tester/>').css({
+      position: 'absolute',
+      top: -9999,
+      left: -9999,
+      width: 'auto',
+      fontSize: input.css('fontSize'),
+      fontFamily: input.css('fontFamily'),
+      fontWeight: input.css('fontWeight'),
+      letterSpacing: input.css('letterSpacing'),
+      whiteSpace: 'nowrap'
+    });
+    let val = '';
+
+    const check = function() {
+      if (val === (val = input.val())) {
+        return;
+      }
+      const escaped = val
+        .replace(/&/g, '&amp;')
+        .replace(/\s/g, ' ')
+        .replace(/</g, '&lt;')
+        .replace(/>/g, '&gt;');
+      testSubject.html(escaped);
+      const testerWidth = testSubject.width(),
+        newWidth = testerWidth + o.comfortZone >= minWidth ? testerWidth + o.comfortZone : minWidth,
+        currentWidth = input.width(),
+        isValidWidthChange =
+          (newWidth < currentWidth && newWidth >= minWidth) ||
+          (newWidth > minWidth && newWidth < o.maxWidth);
+      if (isValidWidthChange) {
+        input.width(newWidth);
+      }
+    };
+    testSubject.insertAfter(element);
+    ko.utils.registerEventHandler(element, 'keyup keydown blur update', check);
+  }
+};

+ 39 - 0
desktop/core/src/desktop/js/ko/bindings/ko.blurHide.js

@@ -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 $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.blurHide = {
+  init: function(element, valueAccessor) {
+    const $el = $(element);
+    const prop = valueAccessor();
+    $el.on('blur', () => {
+      if ($.trim($el.val()) === '') {
+        if (ko.isObservable(prop)) {
+          prop(false);
+        }
+      }
+    });
+  },
+  update: function(element, valueAccessor) {
+    const $el = $(element);
+    const prop = valueAccessor();
+    if (ko.isObservable(prop) && prop()) {
+      $el.focus();
+    }
+  }
+};

+ 55 - 0
desktop/core/src/desktop/js/ko/bindings/ko.bytesize.js

@@ -0,0 +1,55 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+import sprintf from 'sprintf-js';
+
+ko.bindingHandlers.bytesize = (function() {
+  let that;
+  return (that = {
+    units: ['B', 'KB', 'MB', 'GB', 'TB', 'PB'],
+    init: function(element, valueAccessor) {
+      that.format(element, valueAccessor);
+    },
+    update: function(element, valueAccessor) {
+      that.format(element, valueAccessor);
+    },
+    format: function(element, valueAccessor) {
+      const value = valueAccessor();
+      const formatted = that.humanSize(ko.unwrap(value));
+      $(element).text(formatted);
+    },
+    getBaseLog: function(x, y) {
+      return Math.log(x) / Math.log(y);
+    },
+    humanSize: function(bytes) {
+      const isNumber = !isNaN(parseFloat(bytes)) && isFinite(bytes);
+      if (!isNumber) {
+        return '';
+      }
+
+      // Special case small numbers (including 0), because they're exact.
+      if (bytes < 1024) {
+        return sprintf('%d B', bytes);
+      }
+
+      let index = Math.floor(that.getBaseLog(bytes, 1024));
+      index = Math.min(that.units.length - 1, index);
+      return sprintf('%.1f %s', bytes / Math.pow(1024, index), that.units[index]);
+    }
+  });
+})();

+ 42 - 0
desktop/core/src/desktop/js/ko/bindings/ko.chosen.js

@@ -0,0 +1,42 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.chosen = {
+  init: function(element, valueAccessor, allBindings) {
+    const $element = $(element);
+    const options = ko.unwrap(valueAccessor());
+
+    if (typeof options === 'object') {
+      $element.chosen(options);
+    } else {
+      $element.chosen();
+    }
+
+    ['options', 'selectedOptions', 'value'].forEach(propName => {
+      if (allBindings.has(propName)) {
+        const prop = allBindings.get(propName);
+        if (ko.isObservable(prop)) {
+          prop.subscribe(() => {
+            $element.trigger('chosen:updated');
+          });
+        }
+      }
+    });
+  }
+};

+ 95 - 0
desktop/core/src/desktop/js/ko/bindings/ko.clearable.js

@@ -0,0 +1,95 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.clearable = {
+  after: ['textInput', 'value', 'valueUpdate'],
+  init: function(element, valueAccessor, allBindingsAccessor) {
+    const $element = $(element);
+
+    const params = valueAccessor();
+    const valueObservable = ko.isObservable(params) ? params : params.value;
+
+    function tog(v) {
+      return v ? 'addClass' : 'removeClass';
+    }
+
+    $element.addClass('clearable');
+    $element[tog(valueObservable())]('x');
+
+    $element
+      .on('input', function() {
+        $element[tog(this.value)]('x');
+      })
+      .on('mousemove', function(e) {
+        $element[tog(this.offsetWidth - 18 < e.clientX - this.getBoundingClientRect().left)]('onX');
+      })
+      .on('click', function(e) {
+        if (this.offsetWidth - 18 < e.clientX - this.getBoundingClientRect().left) {
+          $element.removeClass('x onX').val('');
+          valueObservable('');
+          if (typeof params.onClear === 'function') {
+            params.onClear();
+          }
+        }
+      });
+
+    if (!allBindingsAccessor()['textInput'] || !allBindingsAccessor()['value']) {
+      $element
+        .on('change', () => {
+          valueObservable($element.val());
+        })
+        .on('blur', () => {
+          valueObservable($element.val());
+        });
+
+      if (
+        allBindingsAccessor()['valueUpdate'] != null &&
+        allBindingsAccessor()['valueUpdate'] === 'afterkeydown'
+      ) {
+        $element.on('keyup', () => {
+          valueObservable($element.val());
+        });
+      }
+
+      if (allBindingsAccessor()['valueUpdateDelay'] != null) {
+        let _timeout = -1;
+        $element.on('keyup', e => {
+          if (!([13, 37, 38, 39, 40].indexOf(e.keyCode) > -1)) {
+            window.clearTimeout(_timeout);
+            _timeout = window.setTimeout(() => {
+              valueObservable($element.val());
+            }, allBindingsAccessor()['valueUpdateDelay']);
+          }
+        });
+      }
+    }
+  },
+  update: function(element, valueAccessor) {
+    const $element = $(element);
+    const params = valueAccessor();
+    const valueObservable = ko.isObservable(params) ? params : params.value;
+
+    if (!$element.is(':focus') || valueObservable() !== $element.val()) {
+      $element.val(valueObservable());
+    }
+    if ($element.val() === '') {
+      $element.removeClass('x');
+    }
+  }
+};

+ 30 - 0
desktop/core/src/desktop/js/ko/bindings/ko.clickForAceFocus.js

@@ -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.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.clickForAceFocus = {
+  init: function(element, valueAccessor) {
+    const editor = valueAccessor();
+    $(element).on('click', e => {
+      if (e.target === element) {
+        editor().focus();
+        editor().execCommand('gotolineend');
+      }
+    });
+  }
+};

+ 32 - 0
desktop/core/src/desktop/js/ko/bindings/ko.clickToCopy.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 $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.clickToCopy = {
+  init: function(element, valueAccessor) {
+    $(element).click(() => {
+      const $input = $('<textarea>')
+        .css({ opacity: 0 })
+        .val(ko.unwrap(valueAccessor()))
+        .appendTo(window.HUE_CONTAINER)
+        .select();
+      document.execCommand('copy');
+      $input.remove();
+    });
+  }
+};

+ 84 - 0
desktop/core/src/desktop/js/ko/bindings/ko.codemirror.js

@@ -0,0 +1,84 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import hueUtils from 'utils/hueUtils';
+
+ko.bindingHandlers.codemirror = {
+  init: function(element, valueAccessor) {
+    const options = $.extend(valueAccessor(), {});
+    const editor = CodeMirror.fromTextArea(element, options);
+    element.editor = editor;
+    editor.setValue(options.data());
+    editor.refresh();
+    const wrapperElement = $(editor.getWrapperElement());
+
+    $(document).on('refreshCodemirror', () => {
+      editor.setSize('100%', 300);
+      editor.refresh();
+    });
+
+    $(document).on('addFieldToSource', (e, field) => {
+      if ($(element).data('template')) {
+        editor.replaceSelection('{{' + field.name() + '}}');
+      }
+    });
+
+    $(document).on('addFunctionToSource', (e, fn) => {
+      if ($(element).data('template')) {
+        editor.replaceSelection(fn);
+      }
+    });
+
+    const $chosenSelect = $('.chosen-select');
+
+    $chosenSelect.chosen({
+      disable_search_threshold: 10,
+      width: '75%'
+    });
+    $chosenSelect.trigger('chosen:updated');
+
+    let sourceDelay = -1;
+    editor.on('change', cm => {
+      clearTimeout(sourceDelay);
+      const _cm = cm;
+      sourceDelay = setTimeout(() => {
+        let _value = _cm.getValue();
+        if (options.stripScript) {
+          _value = _value.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
+        }
+        valueAccessor().data(_value);
+        if (
+          $('.widget-html-pill')
+            .parent()
+            .hasClass('active')
+        ) {
+          $('[contenteditable=true]').html(hueUtils.stripHtmlFromFunctions(valueAccessor().data()));
+        }
+      }, 100);
+    });
+
+    ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
+      wrapperElement.remove();
+    });
+  },
+  update: function(element) {
+    const editor = element.editor;
+    editor.refresh();
+  }
+};

+ 144 - 0
desktop/core/src/desktop/js/ko/bindings/ko.contextMenu.js

@@ -0,0 +1,144 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import huePubSub from 'utils/huePubSub';
+
+/**
+ * This binding can be used to show a custom context menu on right-click,
+ * It assumes that the context menu is nested within the bound element and
+ * the selector for the menu has to be supplied as a parameter.
+ *
+ * Example:
+ *
+ * <div data-bind="contextMenu: {
+ *   menuSelector: '.hue-context-menu',
+ *   beforeOpen: function () { ... }
+ * }">
+ *   <ul class="hue-context-menu">...</ul>
+ * </div>
+ *
+ */
+ko.bindingHandlers.contextMenu = {
+  initContextMenu: function($menu, $scrollContainer) {
+    let active = false;
+
+    let currentLeft = 0;
+    let currentTop = 0;
+    let openScrollTop = 0;
+    let openScrollLeft = 0;
+
+    const adjustForScroll = function() {
+      $menu.css('top', currentTop - $scrollContainer.scrollTop() + openScrollTop);
+      $menu.css('left', currentLeft - $scrollContainer.scrollLeft() + openScrollLeft);
+    };
+
+    return {
+      show: function(event) {
+        $menu.css('top', 0);
+        $menu.css('left', 0);
+        $menu.css('opacity', 0);
+        $menu.show();
+        openScrollTop = $scrollContainer.scrollTop();
+        openScrollLeft = $scrollContainer.scrollLeft();
+        const menuWidth = $menu.outerWidth(true);
+        if (event.clientX + menuWidth > $(window).width()) {
+          currentLeft = $(window).width() - menuWidth;
+        } else {
+          currentLeft = event.clientX;
+        }
+        $menu.css('left', currentLeft);
+
+        const menuHeight = $menu.outerHeight(true);
+        if (event.clientY + menuHeight > $(window).height()) {
+          currentTop = $(window).height() - menuHeight;
+        } else {
+          currentTop = event.clientY;
+        }
+        $menu.css('top', currentTop);
+        $menu.css('opacity', 1);
+        active = true;
+        $scrollContainer.on('scroll', adjustForScroll);
+      },
+      hide: function() {
+        if (active) {
+          $scrollContainer.off('scroll', adjustForScroll);
+          $menu.css('opacity', 0);
+          window.setTimeout(() => {
+            $menu.hide();
+          }, 300);
+          active = false;
+        }
+      }
+    };
+  },
+  init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
+    const $element = $(element);
+    const options = valueAccessor();
+    const $menu = $element.find(options.menuSelector);
+
+    bindingContext.$altDown = ko.observable(false);
+
+    window.addEventListener('keydown', e => {
+      bindingContext.$altDown(e.altKey);
+    });
+
+    window.addEventListener('keyup', () => {
+      bindingContext.$altDown(false);
+    });
+
+    const $scrollContainer =
+      $(options.scrollContainer).length > 0 ? $(options.scrollContainer) : $(window);
+
+    const menu = ko.bindingHandlers.contextMenu.initContextMenu($menu, $scrollContainer);
+
+    element.addEventListener('contextmenu', event => {
+      if (document.selection && document.selection.empty) {
+        document.selection.empty();
+      } else if (window.getSelection) {
+        const sel = window.getSelection();
+        sel.removeAllRanges();
+      }
+      if (typeof options.beforeOpen === 'function') {
+        options.beforeOpen.bind(viewModel)();
+      }
+      menu.show(event);
+      huePubSub.publish('contextmenu-active', element);
+      event.preventDefault();
+      event.stopPropagation();
+    });
+
+    huePubSub.subscribe('contextmenu-active', origin => {
+      if (origin !== element) {
+        menu.hide();
+      }
+    });
+    document.addEventListener('contextmenu', () => {
+      menu.hide();
+    });
+    $menu.click(e => {
+      menu.hide();
+      e.stopPropagation();
+    });
+    $(document).click(event => {
+      if ($element.find($(event.target)).length === 0) {
+        menu.hide();
+      }
+    });
+  }
+};

+ 64 - 0
desktop/core/src/desktop/js/ko/bindings/ko.contextSubMenu.js

@@ -0,0 +1,64 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import huePubSub from 'utils/huePubSub';
+
+ko.bindingHandlers.contextSubMenu = {
+  init: function(element, valueAccessor) {
+    const menuSelector = valueAccessor();
+    const $element = $(element);
+
+    const $menu = $element.find(menuSelector);
+    const $parentMenu = $element.parent('.hue-context-menu');
+    let open = false;
+
+    const closeSubMenu = function() {
+      open = false;
+      $menu.hide();
+      $element.removeClass('active');
+    };
+
+    let hideTimeout = -1;
+    $element.add($menu).on('mouseenter', () => {
+      $element.addClass('active');
+      if (!open) {
+        huePubSub.publish('hue.sub.menu.close');
+      }
+      open = true;
+      window.clearTimeout(hideTimeout);
+      const menuHeight = $menu.outerHeight();
+      $menu.css(
+        'top',
+        $element.position().top + $parentMenu.position().top + menuHeight > $(window).height()
+          ? $(window).height() - menuHeight - 8
+          : $element.position().top + $parentMenu.position().top
+      );
+      $menu.css('left', $element.offset().left + $element.outerWidth(true));
+      $menu.css('opacity', 0);
+      $menu.show();
+      $menu.css('opacity', 1);
+      huePubSub.subscribeOnce('hue.sub.menu.close', closeSubMenu);
+    });
+
+    $element.add($menu).on('mouseleave', () => {
+      window.clearTimeout(hideTimeout);
+      hideTimeout = window.setTimeout(closeSubMenu, 300);
+    });
+  }
+};

+ 98 - 0
desktop/core/src/desktop/js/ko/bindings/ko.datalist.js

@@ -0,0 +1,98 @@
+// 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';
+
+// https://stackoverflow.com/questions/19865364/knockoutjs-linking-value-from-a-input-to-a-datalist-value
+
+ko.bindingHandlers.datalist = (function() {
+  function getVal(rawItem, prop) {
+    const item = ko.unwrap(rawItem);
+    return item && prop ? ko.unwrap(item[prop]) : item;
+  }
+
+  function findItem(options, prop, ref) {
+    return ko.utils.arrayFirst(options, item => {
+      return ref === getVal(item, prop);
+    });
+  }
+
+  return {
+    init: function(element, valueAccessor, allBindingsAccessor) {
+      const setup = valueAccessor(),
+        textProperty = ko.unwrap(setup.optionsText),
+        valueProperty = ko.unwrap(setup.optionsValue),
+        dataItems = ko.unwrap(setup.options),
+        myValue = setup.value,
+        koValue = allBindingsAccessor().value,
+        datalist = document.createElement('DATALIST');
+
+      // create an associated <datalist> element
+      datalist.id = element.getAttribute('list');
+      document.body.appendChild(datalist);
+
+      // when the value is changed, write to the associated myValue observable
+      function onNewValue(newVal) {
+        const setup = valueAccessor(),
+          dataItems = ko.unwrap(setup.options),
+          selectedItem = findItem(dataItems, textProperty, newVal),
+          newValue = selectedItem ? getVal(selectedItem, valueProperty) : newVal;
+
+        if (ko.isWriteableObservable(myValue)) {
+          myValue(newValue);
+        }
+      }
+
+      // listen for value changes
+      // - either via KO's value binding (preferred) or the change event
+      if (ko.isSubscribable(koValue)) {
+        const onNewValueSubscription = koValue.subscribe(onNewValue);
+        ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
+          onNewValueSubscription.remove();
+        });
+      } else {
+        const event = allBindingsAccessor().valueUpdate === 'afterkeydown' ? 'input' : 'change';
+        ko.utils.registerEventHandler(element, event, function() {
+          onNewValue(this.value);
+        });
+      }
+
+      // init the element's value
+      // - either via the myValue observable (preferred) or KO's value binding
+      if (ko.isObservable(myValue) && myValue()) {
+        const selectedItem = findItem(dataItems, valueProperty, myValue());
+        element.value = selectedItem ? getVal(selectedItem, textProperty) : myValue();
+      } else if (ko.isObservable(koValue) && koValue()) {
+        onNewValue(koValue());
+      }
+    },
+    update: function(element, valueAccessor) {
+      const setup = valueAccessor(),
+        datalist = element.list,
+        dataItems = ko.unwrap(setup.options),
+        textProperty = ko.unwrap(setup.optionsText);
+
+      // rebuild list of options when an underlying observable changes
+      datalist.innerHTML = '';
+      ko.utils.arrayForEach(dataItems, item => {
+        const option = document.createElement('OPTION');
+        option.value = getVal(item, textProperty);
+        datalist.appendChild(option);
+      });
+      ko.utils.triggerEvent(element, 'change');
+    }
+  };
+})();

+ 539 - 0
desktop/core/src/desktop/js/ko/bindings/ko.dateRangePicker.js

@@ -0,0 +1,539 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.dateRangePicker = {
+  INTERVAL_OPTIONS: [
+    {
+      value: '+200MILLISECONDS',
+      label: '200ms'
+    },
+    {
+      value: '+1SECONDS',
+      label: '1s'
+    },
+    {
+      value: '+5SECONDS',
+      label: '5s'
+    },
+    {
+      value: '+30SECONDS',
+      label: '30s'
+    },
+    {
+      value: '+1MINUTES',
+      label: '1m'
+    },
+    {
+      value: '+5MINUTES',
+      label: '5m'
+    },
+    {
+      value: '+10MINUTES',
+      label: '10m'
+    },
+    {
+      value: '+30MINUTES',
+      label: '30m'
+    },
+    {
+      value: '+1HOURS',
+      label: '1h'
+    },
+    {
+      value: '+3HOURS',
+      label: '3h'
+    },
+    {
+      value: '+6HOURS',
+      label: '6h'
+    },
+    {
+      value: '+12HOURS',
+      label: '12h'
+    },
+    {
+      value: '+1DAYS',
+      label: '1d'
+    },
+    {
+      value: '+7DAYS',
+      label: '7d'
+    },
+    {
+      value: '+1MONTHS',
+      label: '1M'
+    },
+    {
+      value: '+6MONTHS',
+      label: '6M'
+    },
+    {
+      value: '+1YEARS',
+      label: '1y'
+    },
+    {
+      value: '+10YEARS',
+      label: '10y'
+    }
+  ],
+  EXTRA_INTERVAL_OPTIONS: [],
+
+  init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
+    const DATE_FORMAT = 'YYYY-MM-DD';
+    const TIME_FORMAT = 'HH:mm:ss';
+    const DATETIME_FORMAT = DATE_FORMAT + ' ' + TIME_FORMAT;
+
+    const _el = $(element);
+    const _options = $.extend(valueAccessor(), {});
+
+    const _intervalOptions = [];
+    ko.bindingHandlers.dateRangePicker.INTERVAL_OPTIONS.forEach(interval => {
+      _intervalOptions.push(
+        '<option value="' + interval.value + '">' + interval.label + '</option>'
+      );
+    });
+
+    function enableOptions() {
+      const _opts = [];
+      const _tmp = $('<div>').html(_intervalOptions.join(''));
+      $.each(arguments, (cnt, item) => {
+        if (_tmp.find("option[value='+" + item + "']").length > 0) {
+          _opts.push(
+            '<option value="+' +
+              item +
+              '">' +
+              _tmp
+                .find("option[value='+" + item + "']")
+                .eq(0)
+                .text() +
+              '</option>'
+          );
+        }
+      });
+      return _opts;
+    }
+
+    function renderOptions(opts) {
+      let _html = '';
+      for (let i = 0; i < opts.length; i++) {
+        _html += opts[i];
+      }
+      return _html;
+    }
+
+    const _tmpl = $(
+      '<div class="simpledaterangepicker">' +
+        '<div class="facet-field-cnt custom">' +
+        '<div class="facet-field-label facet-field-label-fixed-width"></div>' +
+        '<div class="facet-field-switch"><i class="fa fa-calendar muted"></i> <a href="javascript:void(0)">' +
+        KO_DATERANGEPICKER_LABELS.DATE_PICKERS +
+        '</a></div>' +
+        '</div>' +
+        '<div class="facet-field-cnt picker">' +
+        '<div class="facet-field-label facet-field-label-fixed-width"></div>' +
+        '<div class="facet-field-switch"><i class="fa fa-calendar-o muted"></i> <a href="javascript:void(0)">' +
+        KO_DATERANGEPICKER_LABELS.CUSTOM_FORMAT +
+        '</a></div>' +
+        '</div>' +
+        '<div class="facet-field-cnt picker">' +
+        '<div class="facet-field-label facet-field-label-fixed-width">' +
+        KO_DATERANGEPICKER_LABELS.START +
+        '</div>' +
+        '<div class="input-prepend input-group">' +
+        '<span class="add-on input-group-addon"><i class="fa fa-calendar"></i></span>' +
+        '<input type="text" class="input-small form-control start-date" />' +
+        '</div>' +
+        '<div class="input-prepend input-group left-margin">' +
+        '<span class="add-on input-group-addon"><i class="fa fa-clock-o"></i></span>' +
+        '<input type="text" class="input-mini form-control start-time" />' +
+        '</div>' +
+        '</div>' +
+        '<div class="facet-field-cnt picker">' +
+        '<div class="facet-field-label facet-field-label-fixed-width">' +
+        KO_DATERANGEPICKER_LABELS.END +
+        '</div>' +
+        '<div class="input-prepend input-group">' +
+        '<span class="add-on input-group-addon"><i class="fa fa-calendar"></i></span>' +
+        '<input type="text" class="input-small form-control end-date" />' +
+        '</div>' +
+        '<div class="input-prepend input-group left-margin">' +
+        '<span class="add-on input-group-addon"><i class="fa fa-clock-o"></i></span>' +
+        '<input type="text" class="input-mini form-control end-time" />' +
+        '</div>' +
+        '</div>' +
+        '<div class="facet-field-cnt picker">' +
+        '<div class="facet-field-label facet-field-label-fixed-width">' +
+        KO_DATERANGEPICKER_LABELS.INTERVAL +
+        '</div>' +
+        '<div class="input-prepend input-group"><span class="add-on input-group-addon"><i class="fa fa-repeat"></i></span></div>&nbsp;' +
+        '<select class="input-small interval-select" style="margin-right: 6px">' +
+        renderOptions(_intervalOptions) +
+        '</select>' +
+        '<input class="input interval hide" type="hidden" value="" />' +
+        '</div>' +
+        '<div class="facet-field-cnt custom">' +
+        '<div class="facet-field-label facet-field-label-fixed-width">' +
+        KO_DATERANGEPICKER_LABELS.START +
+        '</div>' +
+        '<div class="input-prepend input-group">' +
+        '<span class="add-on input-group-addon"><i class="fa fa-calendar-o"></i></span>' +
+        '<input type="text" class="input-large form-control start-date-custom" />' +
+        '</div>' +
+        '<span class="pointer custom-popover" data-trigger="click" data-toggle="popover" data-placement="right" rel="popover" data-html="true"' +
+        '       title="' +
+        KO_DATERANGEPICKER_LABELS.CUSTOM_POPOVER_TITLE +
+        '"' +
+        '       data-content="' +
+        KO_DATERANGEPICKER_LABELS.CUSTOM_POPOVER_CONTENT +
+        '">' +
+        '&nbsp;&nbsp;<i class="fa fa-question-circle"></i>' +
+        ' </span>' +
+        '</div>' +
+        '<div class="facet-field-cnt custom">' +
+        '<div class="facet-field-label facet-field-label-fixed-width">' +
+        KO_DATERANGEPICKER_LABELS.END +
+        '</div>' +
+        '<div class="input-prepend input-group">' +
+        '<span class="add-on input-group-addon"><i class="fa fa-calendar-o"></i></span>' +
+        '<input type="text" class="input-large form-control end-date-custom" />' +
+        '</div>' +
+        '</div>' +
+        '<div class="facet-field-cnt custom">' +
+        '<div class="facet-field-label facet-field-label-fixed-width">' +
+        KO_DATERANGEPICKER_LABELS.INTERVAL +
+        '</div>' +
+        '<div class="input-prepend input-group">' +
+        '<span class="add-on input-group-addon"><i class="fa fa-repeat"></i></span>' +
+        '<input type="text" class="input-large form-control interval-custom" />' +
+        '</div>' +
+        '</div>' +
+        '</div>'
+    );
+
+    _tmpl.insertAfter(_el);
+
+    $('.custom-popover').popover();
+
+    const _minMoment = moment(_options.min());
+    const _maxMoment = moment(_options.max());
+
+    if (_minMoment.isValid() && _maxMoment.isValid()) {
+      _tmpl.find('.facet-field-cnt.custom').hide();
+      _tmpl.find('.facet-field-cnt.picker').show();
+      _tmpl.find('.start-date').val(_minMoment.utc().format(DATE_FORMAT));
+      _tmpl.find('.start-time').val(_minMoment.utc().format(TIME_FORMAT));
+      _tmpl.find('.end-date').val(_maxMoment.utc().format(DATE_FORMAT));
+      _tmpl.find('.end-time').val(_maxMoment.utc().format(TIME_FORMAT));
+      _tmpl.find('.interval').val(_options.gap());
+      _tmpl.find('.interval-select').val(_options.gap());
+      _tmpl.find('.interval-custom').val(_options.gap());
+      if (
+        _tmpl.find('.interval-select').val() == null ||
+        ko.bindingHandlers.dateRangePicker.EXTRA_INTERVAL_OPTIONS.indexOf(
+          _tmpl.find('.interval-select').val()
+        ) > -1
+      ) {
+        pushIntervalValue(_options.gap());
+        _tmpl.find('.facet-field-cnt.custom').show();
+        _tmpl.find('.facet-field-cnt.picker').hide();
+      }
+    } else {
+      _tmpl.find('.facet-field-cnt.custom').show();
+      _tmpl.find('.facet-field-cnt.picker').hide();
+      _tmpl.find('.start-date-custom').val(_options.min());
+      _tmpl.find('.end-date-custom').val(_options.max());
+      _tmpl.find('.interval-custom').val(_options.gap());
+      pushIntervalValue(_options.gap());
+    }
+
+    if (typeof _options.relatedgap != 'undefined') {
+      pushIntervalValue(_options.relatedgap());
+    }
+
+    _tmpl
+      .find('.start-date')
+      .datepicker({
+        format: DATE_FORMAT.toLowerCase()
+      })
+      .on('changeDate', () => {
+        rangeHandler(true);
+      });
+
+    _tmpl.find('.start-date').on('change', () => {
+      rangeHandler(true);
+    });
+
+    _tmpl.find('.start-time').timepicker({
+      minuteStep: 1,
+      showSeconds: true,
+      showMeridian: false,
+      defaultTime: false
+    });
+
+    _tmpl
+      .find('.end-date')
+      .datepicker({
+        format: DATE_FORMAT.toLowerCase()
+      })
+      .on('changeDate', () => {
+        rangeHandler(false);
+      });
+
+    _tmpl.find('.end-date').on('change', () => {
+      rangeHandler(true);
+    });
+
+    _tmpl.find('.end-time').timepicker({
+      minuteStep: 1,
+      showSeconds: true,
+      showMeridian: false,
+      defaultTime: false
+    });
+
+    _tmpl.find('.start-time').on('change', () => {
+      // the timepicker plugin doesn't have a change event handler
+      // so we need to wait a bit to handle in with the default field event
+      window.setTimeout(() => {
+        rangeHandler(true);
+      }, 200);
+    });
+
+    _tmpl.find('.end-time').on('change', () => {
+      window.setTimeout(() => {
+        rangeHandler(false);
+      }, 200);
+    });
+
+    if (_minMoment.isValid() && _maxMoment.isValid()) {
+      rangeHandler(true);
+    }
+
+    _tmpl.find('.facet-field-cnt.picker .facet-field-switch a').on('click', () => {
+      _tmpl.find('.facet-field-cnt.custom').show();
+      _tmpl.find('.facet-field-cnt.picker').hide();
+    });
+
+    _tmpl.find('.facet-field-cnt.custom .facet-field-switch a').on('click', () => {
+      _tmpl.find('.facet-field-cnt.custom').hide();
+      _tmpl.find('.facet-field-cnt.picker').show();
+    });
+
+    _tmpl.find('.start-date-custom').on('change', () => {
+      _options.min(_tmpl.find('.start-date-custom').val());
+      _tmpl.find('.start-date').val(
+        moment(_options.min())
+          .utc()
+          .format(DATE_FORMAT)
+      );
+      _tmpl.find('.start-time').val(
+        moment(_options.min())
+          .utc()
+          .format(TIME_FORMAT)
+      );
+      _options.start(_options.min());
+    });
+
+    _tmpl.find('.end-date-custom').on('change', () => {
+      _options.max(_tmpl.find('.end-date-custom').val());
+      _tmpl.find('.end-date').val(
+        moment(_options.max())
+          .utc()
+          .format(DATE_FORMAT)
+      );
+      _tmpl.find('.end-time').val(
+        moment(_options.max())
+          .utc()
+          .format(TIME_FORMAT)
+      );
+      _options.end(_options.max());
+    });
+
+    _tmpl.find('.interval-custom').on('change', () => {
+      _options.gap(_tmpl.find('.interval-custom').val());
+      matchIntervals(true);
+      if (typeof _options.relatedgap != 'undefined') {
+        _options.relatedgap(_options.gap());
+      }
+    });
+
+    function pushIntervalValue(newValue) {
+      let _found = false;
+      ko.bindingHandlers.dateRangePicker.INTERVAL_OPTIONS.forEach(interval => {
+        if (interval.value == newValue) {
+          _found = true;
+        }
+      });
+      if (!_found) {
+        ko.bindingHandlers.dateRangePicker.INTERVAL_OPTIONS.push({
+          value: newValue,
+          label: newValue
+        });
+        ko.bindingHandlers.dateRangePicker.EXTRA_INTERVAL_OPTIONS.push(newValue);
+        _intervalOptions.push('<option value="' + newValue + '">' + newValue + '</option>');
+      }
+    }
+
+    function matchIntervals(fromCustom) {
+      _tmpl.find('.interval-select').val(_options.gap());
+      if (_tmpl.find('.interval-select').val() == null) {
+        if (fromCustom) {
+          pushIntervalValue(_options.gap());
+          if (bindingContext.$root.intervalOptions) {
+            bindingContext.$root.intervalOptions(
+              ko.bindingHandlers.dateRangePicker.INTERVAL_OPTIONS
+            );
+          }
+        } else {
+          _tmpl.find('.interval-select').val(_tmpl.find('.interval-select option:first').val());
+          _options.gap(_tmpl.find('.interval-select').val());
+          if (typeof _options.relatedgap != 'undefined') {
+            _options.relatedgap(_options.gap());
+          }
+          _tmpl.find('.interval-custom').val(_options.gap());
+        }
+      }
+    }
+
+    _tmpl.find('.interval-select').on('change', () => {
+      _options.gap(_tmpl.find('.interval-select').val());
+      if (typeof _options.relatedgap != 'undefined') {
+        _options.relatedgap(_options.gap());
+      }
+      _tmpl.find('.interval').val(_options.gap());
+      _tmpl.find('.interval-custom').val(_options.gap());
+    });
+
+    function rangeHandler(isStart) {
+      const startDate = moment(
+        _tmpl.find('.start-date').val() + ' ' + _tmpl.find('.start-time').val(),
+        DATETIME_FORMAT
+      );
+      const endDate = moment(
+        _tmpl.find('.end-date').val() + ' ' + _tmpl.find('.end-time').val(),
+        DATETIME_FORMAT
+      );
+      if (startDate.valueOf() > endDate.valueOf()) {
+        if (isStart) {
+          _tmpl.find('.end-date').val(startDate.utc().format(DATE_FORMAT));
+          _tmpl.find('.end-date').datepicker('setValue', startDate.utc().format(DATE_FORMAT));
+          _tmpl.find('.end-date').data('original-val', _tmpl.find('.end-date').val());
+          _tmpl.find('.end-time').val(startDate.utc().format(TIME_FORMAT));
+        } else {
+          if (_tmpl.find('.end-date').val() === _tmpl.find('.start-date').val()) {
+            _tmpl.find('.end-time').val(startDate.utc().format(TIME_FORMAT));
+            _tmpl
+              .find('.end-time')
+              .data('timepicker')
+              .setValues(startDate.format(TIME_FORMAT));
+          } else {
+            _tmpl.find('.end-date').val(_tmpl.find('.end-date').data('original-val'));
+            _tmpl
+              .find('.end-date')
+              .datepicker('setValue', _tmpl.find('.end-date').data('original-val'));
+          }
+          // non-sticky error notification
+          $.jHueNotify.notify({
+            level: 'ERROR',
+            message: 'The end cannot be before the starting moment'
+          });
+        }
+      } else {
+        _tmpl.find('.end-date').data('original-val', _tmpl.find('.end-date').val());
+        _tmpl.find('.start-date').datepicker('hide');
+        _tmpl.find('.end-date').datepicker('hide');
+      }
+
+      const _calculatedStartDate = moment(
+        _tmpl.find('.start-date').val() + ' ' + _tmpl.find('.start-time').val(),
+        DATETIME_FORMAT
+      );
+      const _calculatedEndDate = moment(
+        _tmpl.find('.end-date').val() + ' ' + _tmpl.find('.end-time').val(),
+        DATETIME_FORMAT
+      );
+
+      _options.min(_calculatedStartDate.format('YYYY-MM-DD[T]HH:mm:ss[Z]'));
+      _options.start(_options.min());
+      _options.max(_calculatedEndDate.format('YYYY-MM-DD[T]HH:mm:ss[Z]'));
+      _options.end(_options.max());
+
+      _tmpl.find('.start-date-custom').val(_options.min());
+      _tmpl.find('.end-date-custom').val(_options.max());
+
+      let _opts = [];
+      // hide not useful options from interval
+      if (
+        _calculatedEndDate.diff(_calculatedStartDate, 'minutes') > 1 &&
+        _calculatedEndDate.diff(_calculatedStartDate, 'minutes') <= 60
+      ) {
+        _opts = enableOptions(
+          '200MILLISECONDS',
+          '1SECONDS',
+          '1MINUTES',
+          '5MINUTES',
+          '10MINUTES',
+          '30MINUTES'
+        );
+      }
+      if (
+        _calculatedEndDate.diff(_calculatedStartDate, 'hours') > 1 &&
+        _calculatedEndDate.diff(_calculatedStartDate, 'hours') <= 12
+      ) {
+        _opts = enableOptions('5MINUTES', '10MINUTES', '30MINUTES', '1HOURS', '3HOURS');
+      }
+      if (
+        _calculatedEndDate.diff(_calculatedStartDate, 'hours') > 12 &&
+        _calculatedEndDate.diff(_calculatedStartDate, 'hours') < 36
+      ) {
+        _opts = enableOptions('10MINUTES', '30MINUTES', '1HOURS', '3HOURS', '6HOURS', '12HOURS');
+      }
+      if (
+        _calculatedEndDate.diff(_calculatedStartDate, 'days') > 1 &&
+        _calculatedEndDate.diff(_calculatedStartDate, 'days') <= 7
+      ) {
+        _opts = enableOptions('30MINUTES', '1HOURS', '3HOURS', '6HOURS', '12HOURS', '1DAYS');
+      }
+      if (
+        _calculatedEndDate.diff(_calculatedStartDate, 'days') > 7 &&
+        _calculatedEndDate.diff(_calculatedStartDate, 'days') <= 14
+      ) {
+        _opts = enableOptions('3HOURS', '6HOURS', '12HOURS', '1DAYS');
+      }
+      if (
+        _calculatedEndDate.diff(_calculatedStartDate, 'days') > 14 &&
+        _calculatedEndDate.diff(_calculatedStartDate, 'days') <= 31
+      ) {
+        _opts = enableOptions('12HOURS', '1DAYS', '7DAYS');
+      }
+      if (_calculatedEndDate.diff(_calculatedStartDate, 'months') >= 1) {
+        _opts = enableOptions('1DAYS', '7DAYS', '1MONTHS');
+      }
+      if (_calculatedEndDate.diff(_calculatedStartDate, 'months') > 6) {
+        _opts = enableOptions('1DAYS', '7DAYS', '1MONTHS', '6MONTHS');
+      }
+      if (_calculatedEndDate.diff(_calculatedStartDate, 'months') > 12) {
+        _opts = enableOptions('7DAYS', '1MONTHS', '6MONTHS', '1YEARS', '10YEARS');
+      }
+
+      $('.interval-select').html(renderOptions(_opts));
+
+      matchIntervals(true);
+    }
+  }
+};

+ 56 - 0
desktop/core/src/desktop/js/ko/bindings/ko.datepicker.js

@@ -0,0 +1,56 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.datepicker = {
+  init: function(element, valueAccessor, allBindings) {
+    const _el = $(element);
+    const _options = ko.unwrap(valueAccessor());
+    _el
+      .datepicker({
+        format: 'yyyy-mm-dd'
+      })
+      .on('show', e => {
+        if (_options.momentFormat) {
+          const m = moment.utc(_el.val());
+          _el.datepicker('setValue', m.format('YYYY-MM-DD'));
+          _el.val(m.format(_options.momentFormat)); // Set value again as datepicker clears the time component
+        }
+      })
+      .on('changeDate', e => {
+        setDate(e.date);
+      })
+      .on('hide', e => {
+        setDate(e.date);
+      });
+
+    function setDate(d) {
+      if (_options.momentFormat) {
+        const m = moment(d);
+        // Keep time intact
+        const previous = moment.utc(allBindings().value());
+        previous.date(m.date());
+        previous.month(m.month());
+        previous.year(m.year());
+
+        _el.val(previous.format(_options.momentFormat));
+      }
+      allBindings().value(_el.val());
+    }
+  }
+};

+ 28 - 0
desktop/core/src/desktop/js/ko/bindings/ko.dblClick.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 $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.dblclick = {
+  init: function(element, valueAccessor, allBindingsAccessor, viewModel) {
+    const fn = valueAccessor();
+    $(element).dblclick(function() {
+      const data = ko.dataFor(this);
+      fn.call(viewModel, data);
+    });
+  }
+};

+ 49 - 0
desktop/core/src/desktop/js/ko/bindings/ko.delayedOverflow.js

@@ -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 $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.delayedOverflow = {
+  init: function(element, valueAccessor) {
+    const $element = $(element);
+
+    $element.css('overflow', 'hidden');
+
+    let isTouch = false;
+    $element.on('touchstart', () => {
+      isTouch = true;
+    });
+
+    let scrollTimeout = -1;
+    $element.hover(
+      () => {
+        scrollTimeout = window.setTimeout(
+          () => {
+            $element.css('overflow', 'auto');
+          },
+          valueAccessor && valueAccessor() === 'slow' ? 500 : 30
+        );
+      },
+      () => {
+        if (!isTouch) {
+          clearTimeout(scrollTimeout);
+          $element.css('overflow', 'hidden');
+        }
+      }
+    );
+  }
+};

+ 88 - 0
desktop/core/src/desktop/js/ko/bindings/ko.dockable.js

@@ -0,0 +1,88 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import huePubSub from 'utils/huePubSub';
+import hueUtils from 'utils/hueUtils';
+
+ko.bindingHandlers.dockable = {
+  init: function(element, valueAccessor) {
+    const options = valueAccessor() || {};
+    const scrollable = options.scrollable ? options.scrollable : window;
+    const triggerAdjust = options.triggerAdjust || 0;
+    const zIndex = options.zIndex || 1000;
+    $(element).addClass('dockable');
+
+    let initialTopPosition = -1;
+    let initialSize = {
+      w: $(element).width() - hueUtils.scrollbarWidth(),
+      h: $(element).outerHeight() + (options.jumpCorrection || 0)
+    };
+
+    const ghost = $('<div>')
+      .css({ display: 'none', height: initialSize.h })
+      .insertBefore($(element));
+
+    function dock() {
+      if (initialTopPosition === -1) {
+        initialTopPosition = $(element).position().top;
+        ghost.height($(element).outerHeight() + (options.jumpCorrection || 0));
+      }
+      if ($(scrollable).scrollTop() + triggerAdjust > initialTopPosition) {
+        $(element).attr(
+          'style',
+          'position: fixed!important; top: ' +
+            options.topSnap +
+            '; width: ' +
+            initialSize.w +
+            'px!important; z-index: ' +
+            zIndex
+        );
+        ghost.show();
+      } else {
+        $(element).removeAttr('style');
+        ghost.hide();
+      }
+    }
+
+    $(scrollable).on('scroll', dock);
+
+    const scrollOffSubscription = huePubSub.subscribe('scrollable.scroll.off', scrollElement => {
+      if (scrollElement === scrollable) {
+        $(scrollable).on('scroll', dock);
+      }
+    });
+
+    function resetInitialStyle() {
+      $(element).removeAttr('style');
+      initialSize = {
+        w: $(element).width() - hueUtils.scrollbarWidth(),
+        h: $(element).outerHeight() + (options.jumpCorrection || 0)
+      };
+      dock();
+    }
+
+    $(window).on('resize', resetInitialStyle);
+
+    ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
+      $(window).off('resize', resetInitialStyle);
+      $(scrollable).off('scroll', dock);
+      scrollOffSubscription.remove();
+    });
+  }
+};

+ 125 - 0
desktop/core/src/desktop/js/ko/bindings/ko.documentChooser.js

@@ -0,0 +1,125 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import apiHelper from 'api/apiHelper';
+
+const TYPE_MAP = {
+  hive: 'query-hive',
+  impala: 'query-impala',
+  java: 'query-java',
+  spark: 'query-spark2',
+  pig: 'query-pig',
+  sqoop: 'query-sqoop1',
+  'distcp-doc': 'query-distcp',
+  'shell-doc': 'query-shell',
+  'mapreduce-doc': 'query-mapreduce',
+  'hive-document-widget': 'query-hive',
+  'impala-document-widget': 'query-impala',
+  'java-document-widget': 'query-java',
+  'spark-document-widget': 'query-spark2',
+  'pig-document-widget': 'query-pig',
+  'sqoop-document-widget': 'query-sqoop1',
+  'distcp-document-widget': 'query-distcp',
+  'shell-document-widget': 'query-shell',
+  'mapreduce-document-widget': 'query-mapreduce'
+};
+
+ko.bindingHandlers.documentChooser = {
+  init: function(element, valueAccessor) {
+    const options = valueAccessor();
+    let type = 'query-hive';
+
+    if (options.type) {
+      let tempType = ko.unwrap(options.type);
+      if (tempType === 'function') {
+        tempType = tempType();
+      }
+      type = TYPE_MAP[tempType] ? TYPE_MAP[tempType] : tempType;
+    }
+    let firstLoad = false;
+
+    $(element).selectize({
+      valueField: 'uuid',
+      labelField: 'name',
+      searchField: 'name',
+      options: [],
+      create: false,
+      preload: true,
+      dropdownParent: 'body',
+      render: {
+        option: function(item, escape) {
+          return (
+            '<div>' +
+            '<strong>' +
+            escape(item.name) +
+            '</strong><br>' +
+            '<span class="muted">' +
+            escape(item.description) +
+            '</span>' +
+            '</div>'
+          );
+        }
+      },
+      load: function(query, callback) {
+        if (query === '' && options.value && !firstLoad) {
+          firstLoad = true;
+        }
+        apiHelper.searchDocuments({
+          type: type,
+          text: query,
+          include_trashed: false,
+          limit: 100,
+          successCallback: function(data) {
+            callback(data.documents);
+          }
+        });
+      },
+      onChange: function(val) {
+        if (options.value) {
+          options.value(val);
+        }
+        if (options.document) {
+          options.document(this.options[val]);
+        }
+        if (options.mappedDocument) {
+          options.mappedDocument(ko.mapping.fromJS(this.options[val]));
+        }
+      },
+      onLoad: function() {
+        if (options.value) {
+          this.setValue(options.value());
+        }
+        if (options.loading) {
+          options.loading(false);
+        }
+      }
+    });
+  },
+
+  update: function(element, valueAccessor) {
+    const options = valueAccessor();
+    if (options.value) {
+      element.selectize.setValue(options.value());
+    }
+    if (options.dependentValue && options.dependentValue() !== '') {
+      element.selectize.setValue(options.dependentValue());
+      options.dependentValue('');
+    }
+  }
+};

+ 86 - 0
desktop/core/src/desktop/js/ko/bindings/ko.documentContextPopover.js

@@ -0,0 +1,86 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import apiHelper from 'api/apiHelper';
+import huePubSub from 'utils/huePubSub';
+
+/**
+ * Show the Context Popover for Documents when the bound element is clicked.
+ *
+ * Parameters:
+ *
+ * {string} uuid - the uuid of the document
+ * {string} [orientation] - 'top', 'right', 'bottom', 'left'. Default 'right'
+ * {Object} [offset] - Optional offset from the element
+ * {number} [offset.top] - Offset in pixels
+ * {number} [offset.left] - Offset in pixels
+ *
+ * Examples:
+ *
+ * data-bind="documentContextPopover: { uuid: 'bana-na12-3456-7890' }"
+ * data-bind="documentContextPopover: { uuid: 'bana-na12-3456-7890', orientation: 'bottom', offset: { top: 5 } }"
+ *
+ * @type {{init: ko.bindingHandlers.documentContextPopover.init}}
+ */
+ko.bindingHandlers.documentContextPopover = {
+  init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
+    ko.bindingHandlers.click.init(
+      element,
+      () => {
+        return function() {
+          const options = valueAccessor();
+
+          apiHelper
+            .fetchDocument({
+              uuid: options.uuid,
+              fetchContents: true,
+              silenceErrors: true
+            })
+            .done(response => {
+              const $source = $(element);
+              const offset = $source.offset();
+              if (options.offset) {
+                offset.top += options.offset.top || 0;
+                offset.left += options.offset.left || 0;
+              }
+
+              huePubSub.publish('context.popover.show', {
+                data: {
+                  type: 'hue',
+                  definition: response.document
+                },
+                showInAssistEnabled: true,
+                orientation: options.orientation || 'right',
+                source: {
+                  element: element,
+                  left: offset.left,
+                  top: offset.top,
+                  right: offset.left + $source.width(),
+                  bottom: offset.top + $source.height()
+                }
+              });
+            });
+        };
+      },
+      allBindings,
+      viewModel,
+      bindingContext
+    );
+  }
+};

+ 83 - 0
desktop/core/src/desktop/js/ko/bindings/ko.draggableText.js

@@ -0,0 +1,83 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import huePubSub from 'utils/huePubSub';
+
+ko.bindingHandlers.draggableText = {
+  init: function(element, valueAccessor) {
+    const $element = $(element);
+    const options = valueAccessor();
+    if ((ko.isObservable(options.text) && !options.text()) || !options.text) {
+      return;
+    }
+    $element.addClass('draggableText');
+
+    const $helper = $('<div>')
+      .text(ko.isObservable(options.text) ? options.text() : options.text)
+      .css('z-index', '99999');
+    let dragStartX = -1;
+    let dragStartY = -1;
+    let notifiedOnDragStarted = false;
+    $element.draggable({
+      helper: function() {
+        return $helper;
+      },
+      appendTo: 'body',
+      start: function(event) {
+        dragStartX = event.clientX;
+        dragStartY = event.clientY;
+        huePubSub.publish('draggable.text.meta', options.meta);
+        notifiedOnDragStarted = false;
+      },
+      drag: function(event) {
+        huePubSub.publish('draggable.text.drag', {
+          event: event,
+          meta: options.meta
+        });
+        if (
+          !notifiedOnDragStarted &&
+          Math.sqrt(
+            (dragStartX - event.clientX) * (dragStartX - event.clientX) +
+              (dragStartY - event.clientY) * (dragStartY - event.clientY)
+          ) >= 10
+        ) {
+          huePubSub.publish('draggable.text.started', options.meta);
+          notifiedOnDragStarted = true;
+        }
+      },
+      stop: function(event) {
+        if (
+          Math.sqrt(
+            (dragStartX - event.clientX) * (dragStartX - event.clientX) +
+              (dragStartY - event.clientY) * (dragStartY - event.clientY)
+          ) < 10
+        ) {
+          $helper.remove();
+          const elementAtStart = document.elementFromPoint(dragStartX, dragStartY);
+          const elementAtStop = document.elementFromPoint(event.clientX, event.clientY);
+          if (elementAtStart === elementAtStop) {
+            $(elementAtStop).trigger('click');
+          }
+        }
+        notifiedOnDragStarted = false;
+        huePubSub.publish('draggable.text.stopped');
+      }
+    });
+  }
+};

+ 24 - 0
desktop/core/src/desktop/js/ko/bindings/ko.dropdown.js

@@ -0,0 +1,24 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.dropdown = {
+  init: function(element) {
+    $(element).dropdown();
+  }
+};

+ 115 - 0
desktop/core/src/desktop/js/ko/bindings/ko.dropzone.js

@@ -0,0 +1,115 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+// TODO: Depends on Dropzone
+
+ko.bindingHandlers.dropzone = {
+  init: function(element, valueAccessor) {
+    const value = ko.unwrap(valueAccessor());
+    if (value.disabled) {
+      return;
+    }
+    const options = {
+      autoDiscover: false,
+      maxFilesize: 5000000,
+      previewsContainer: '#progressStatusContent',
+      previewTemplate:
+        '<div class="progress-row">' +
+        '<span class="break-word" data-dz-name></span>' +
+        '<div class="pull-right">' +
+        '<span class="muted" data-dz-size></span>&nbsp;&nbsp;' +
+        '<span data-dz-remove><a href="javascript:undefined;" title="' +
+        HUE_I18n.dropzone.cancelUpload +
+        '"><i class="fa fa-fw fa-times"></i></a></span>' +
+        '<span style="display: none" data-dz-uploaded><i class="fa fa-fw fa-check muted"></i></span>' +
+        '</div>' +
+        '<div class="progress-row-bar" data-dz-uploadprogress></div>' +
+        '</div>',
+      sending: function() {
+        $('.hoverMsg').addClass('hide');
+        $('#progressStatus').removeClass('hide');
+        $('#progressStatusBar').removeClass('hide');
+        $('#progressStatus .progress-row').remove();
+        $('#progressStatusBar div').css('width', '0');
+      },
+      uploadprogress: function(file, progress) {
+        $('[data-dz-name]').each((cnt, item) => {
+          if ($(item).text() === file.name) {
+            $(item)
+              .parents('.progress-row')
+              .find('[data-dz-uploadprogress]')
+              .width(progress.toFixed() + '%');
+            if (progress.toFixed() === '100') {
+              $(item)
+                .parents('.progress-row')
+                .find('[data-dz-remove]')
+                .hide();
+              $(item)
+                .parents('.progress-row')
+                .find('[data-dz-uploaded]')
+                .show();
+            }
+          }
+        });
+      },
+      totaluploadprogress: function(progress) {
+        $('#progressStatusBar div').width(progress.toFixed() + '%');
+      },
+      canceled: function() {
+        $.jHueNotify.info(HUE_I18n.dropzone.uploadCanceled);
+      },
+      complete: function(file) {
+        if (file.xhr.response !== '') {
+          const response = JSON.parse(file.xhr.response);
+          if (response && response.status != null) {
+            if (response.status !== 0) {
+              $(document).trigger('error', response.data);
+              if (value.onError) {
+                value.onError(file.name);
+              }
+            } else {
+              $(document).trigger('info', response.path + ' ' + HUE_I18n.dropzone.uploadSucceeded);
+              if (value.onComplete) {
+                value.onComplete(response.path);
+              }
+            }
+          }
+        }
+      },
+      queuecomplete: function() {
+        window.setTimeout(() => {
+          $('#progressStatus').addClass('hide');
+          $('#progressStatusBar').addClass('hide');
+          $('#progressStatusBar div').css('width', '0');
+        }, 2500);
+      },
+      createImageThumbnails: false
+    };
+
+    $.extend(options, value);
+
+    $(element).addClass('dropzone');
+
+    $(element).on('click', e => {
+      e.stopPropagation();
+    });
+
+    new Dropzone(element, options);
+  }
+};

+ 75 - 0
desktop/core/src/desktop/js/ko/bindings/ko.duration.js

@@ -0,0 +1,75 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+import sprintf from 'sprintf-js';
+
+ko.bindingHandlers.duration = (function() {
+  let that;
+  return (that = {
+    init: function(element, valueAccessor) {
+      that.format(element, valueAccessor);
+    },
+    update: function(element, valueAccessor) {
+      that.format(element, valueAccessor);
+    },
+    format: function(element, valueAccessor) {
+      const value = valueAccessor();
+      const formatted = that.humanTime(ko.unwrap(value));
+      $(element).text(formatted);
+    },
+    humanTime: function(value) {
+      value = value * 1;
+      if (value < Math.pow(10, 3)) {
+        return sprintf('%i ns', value);
+      } else if (value - Math.pow(10, 6) < -Math.pow(10, 3) / 2) {
+        // Make sure rounding doesn't cause numbers to have more than 4 significant digits.
+        value = (value * 1.0) / Math.pow(10, 3);
+        const sprint = value > 100 ? '%i us' : '%.1f us';
+        return sprintf(sprint, value);
+      } else if (value - Math.pow(10, 9) < -Math.pow(10, 6) / 2) {
+        value = (value * 1.0) / Math.pow(10, 6);
+        const sprint = value > 100 ? '%i ms' : '%.1f ms';
+        return sprintf(sprint, value);
+      } else {
+        // get the ms value
+        const SECOND = 1;
+        const MINUTE = SECOND * 60;
+        const HOUR = MINUTE * 60;
+        value = (value * 1) / Math.pow(10, 9);
+        let buffer = '';
+
+        if (value > HOUR) {
+          buffer += sprintf('%i h', value / HOUR);
+          value = value % HOUR;
+        }
+
+        if (buffer.length < 4 && value > MINUTE) {
+          const sprint = buffer.length ? ' %i m' : '%i m';
+          buffer += sprintf(sprint, value / MINUTE);
+          value = value % MINUTE;
+        }
+
+        if (buffer.length < 4 && value > SECOND) {
+          const sprint = buffer.length ? ' %i s' : '%.1f s';
+          buffer += sprintf(sprint, (value * 1.0) / SECOND);
+        }
+        return buffer;
+      }
+    }
+  });
+})();

+ 33 - 0
desktop/core/src/desktop/js/ko/bindings/ko.ellipsis.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 $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.ellipsis = {
+  update: function(element, valueAccessor) {
+    const value = ko.unwrap(valueAccessor());
+    const $element = $(element);
+    const chopLength = value.length ? value.length : 30;
+    const text = typeof value === 'object' ? value.data : value;
+    if (text.length > chopLength) {
+      $element.attr('title', text);
+      $element.text(text.substr(0, chopLength) + '...');
+    } else {
+      $element.text(text);
+    }
+  }
+};

+ 40 - 0
desktop/core/src/desktop/js/ko/bindings/ko.fadeVisible.js

@@ -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.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.fadeVisible = {
+  init: function(element, valueAccessor) {
+    const value = valueAccessor();
+    const toggleValue = typeof value.value === 'undefined' ? value : value.value;
+    $(element).toggle(ko.unwrap(toggleValue));
+  },
+  update: function(element, valueAccessor) {
+    const value = valueAccessor();
+    const toggleValue = typeof value.value === 'undefined' ? value : value.value;
+    const speed = typeof value.speed === 'undefined' ? 'normal' : value.speed;
+    const fadeOut = typeof value.fadeOut === 'undefined' ? false : value.fadeOut;
+    $(element).stop();
+    if (ko.unwrap(toggleValue)) {
+      $(element).fadeIn(speed);
+    } else if (fadeOut) {
+      $(element).fadeOut(speed);
+    } else {
+      $(element).hide();
+    }
+  }
+};

+ 43 - 0
desktop/core/src/desktop/js/ko/bindings/ko.fetchMore.js

@@ -0,0 +1,43 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.fetchMore = {
+  init: function(element, valueAccessor) {
+    const options = valueAccessor();
+    const $element = $(element);
+
+    let throttle = -1;
+    $element.on('scroll.fetchMore', () => {
+      window.clearTimeout(throttle);
+      throttle = window.setTimeout(() => {
+        if (
+          element.scrollTop + $element.innerHeight() >= element.scrollHeight - 10 &&
+          ko.unwrap(options.hasMore) &&
+          !ko.unwrap(options.loadingMore)
+        ) {
+          options.fetchMore();
+        }
+      }, 100);
+    });
+
+    ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
+      $element.off('scroll.fetchMore');
+    });
+  }
+};

+ 99 - 0
desktop/core/src/desktop/js/ko/bindings/ko.fileChooser.js

@@ -0,0 +1,99 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import hueUtils from 'utils/hueUtils';
+
+ko.bindingHandlers.filechooser = {
+  init: function(element, valueAccessor, allBindingsAccessor) {
+    const $element = $(element);
+    const options = ko.unwrap(allBindingsAccessor());
+    $element.attr('autocomplete', 'off');
+    if (typeof valueAccessor() == 'function' || typeof valueAccessor().value == 'function') {
+      $element.val(valueAccessor().value ? valueAccessor().value() : valueAccessor()());
+      $element.data('fullPath', $element.val());
+      $element.attr('data-original-title', $element.val());
+      if (valueAccessor().displayJustLastBit) {
+        const _val = $element.val();
+        $element.val(_val.split('/')[_val.split('/').length - 1]);
+      }
+      $element.on('blur', () => {
+        if (valueAccessor().value) {
+          if (valueAccessor().displayJustLastBit) {
+            const _val = $element.data('fullPath');
+            valueAccessor().value(_val.substr(0, _val.lastIndexOf('/')) + '/' + $element.val());
+          } else {
+            valueAccessor().value($element.val());
+          }
+          $element.data('fullPath', valueAccessor().value());
+        } else {
+          valueAccessor()($element.val());
+          $element.data('fullPath', valueAccessor()());
+        }
+        $element.attr('data-original-title', $element.data('fullPath'));
+      });
+
+      if (options.valueUpdate && options.valueUpdate === 'afterkeydown') {
+        $element.on('keyup', () => {
+          if (valueAccessor().value) {
+            valueAccessor().value($element.val());
+          } else {
+            valueAccessor()($element.val());
+          }
+        });
+      }
+    } else {
+      $element.val(valueAccessor());
+      $element.on('blur', () => {
+        valueAccessor($element.val());
+      });
+      if (options.valueUpdate && options.valueUpdate === 'afterkeydown') {
+        $element.on('keyup', () => {
+          valueAccessor($element.val());
+        });
+      }
+    }
+
+    $element.after(
+      hueUtils.getFileBrowseButton(
+        $element,
+        true,
+        valueAccessor,
+        true,
+        allBindingsAccessor,
+        valueAccessor().isAddon,
+        valueAccessor().isNestedModal,
+        allBindingsAccessor &&
+          allBindingsAccessor().filechooserOptions &&
+          allBindingsAccessor().filechooserOptions.linkMarkup
+      )
+    );
+
+    if (
+      allBindingsAccessor &&
+      allBindingsAccessor().filechooserOptions &&
+      allBindingsAccessor().filechooserOptions.openOnFocus
+    ) {
+      $element.on('focus', () => {
+        if ($element.val() === '') {
+          $element.siblings('.filechooser-clickable').click();
+        }
+      });
+    }
+  }
+};

+ 526 - 0
desktop/core/src/desktop/js/ko/bindings/ko.foreachVisible.js

@@ -0,0 +1,526 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import huePubSub from 'utils/huePubSub';
+
+/**
+ * This binding limits the rendered items based on what is visible within a scrollable container. It supports
+ * multiple any amount of nested children with foreachVisible bindings
+ *
+ * The minHeight parameter is the initial expected rendered height of each entry, once rendered the real
+ * height is used. It keeps a number of elements above and below the visible elements to make slow scrolling
+ * smooth.
+ *
+ * The height of the container element has to be less than or equal to the inner height of the window.
+ *
+ * Example:
+ *
+ * <div class="container" style="overflow-y: scroll; height: 100px">
+ *  <ul data-bind="foreachVisible: { data: items, minHeight: 20, container: '.container' }">
+ *    <li>...</li>
+ *  </ul>
+ * </div>
+ *
+ * For tables the binding has to be attached to the tbody element:
+ *
+ * <div class="container" style="overflow-y: scroll; height: 100px">
+ *  <table>
+ *    <thead>...</thead>
+ *    <tbody data-bind="foreachVisible: { data: items, minHeight: 20, container: '.container' }>
+ *      <tr>...</tr>
+ *    </tbody>
+ *  </ul>
+ * </div>
+ *
+ * Currently the binding only supports one element inside the bound element otherwise the height
+ * calculations will be off. In other words this will make it go bonkers:
+ *
+ * <div class="container" style="overflow-y: scroll; height: 100px">
+ *  <ul data-bind="foreachVisible: { data: items, minHeight: 20, container: '.container' }">
+ *    <li>...</li>
+ *    <li style="display: none;">...</li>
+ *  </ul>
+ * </div>
+ *
+ */
+ko.bindingHandlers.foreachVisible = {
+  init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
+    return ko.bindingHandlers.template.init(
+      element,
+      () => {
+        return {
+          foreach: [],
+          templateEngine: ko.nativeTemplateEngine.instance
+        };
+      },
+      allBindings,
+      viewModel,
+      bindingContext
+    );
+  },
+
+  update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
+    const options = valueAccessor();
+    let $element = $(element);
+    let isTable = false;
+
+    if ($element.parent().is('table')) {
+      $element = $element.parent();
+      isTable = true;
+    }
+
+    const $container = $element.closest(options.container);
+
+    const id = Math.random();
+
+    // This is possibly a parent element that has the foreachVisible binding
+    const $parentFVElement = bindingContext.$parentForeachVisible || null;
+    const parentId = bindingContext.$parentForeachVisibleId || -1;
+    // This is the element from the parent foreachVisible rendered element that contains
+    // this one or container for root
+    let $parentFVOwnerElement = $container;
+    $element.data('parentForeachVisible', $parentFVElement);
+
+    const depth = bindingContext.$depth || 0;
+
+    // Locate the owning element if within another foreach visible binding
+    if ($parentFVElement) {
+      const myOffset = $element.offset().top;
+      $parentFVElement.children().each((idx, child) => {
+        const $child = $(child);
+        if (myOffset > $child.offset().top) {
+          $parentFVOwnerElement = $child;
+        } else {
+          return false;
+        }
+      });
+    }
+
+    if ($parentFVOwnerElement.data('disposalFunction')) {
+      $parentFVOwnerElement.data('disposalFunction')();
+      $parentFVOwnerElement.data('lastKnownHeights', null);
+    }
+
+    let entryMinHeight = options.minHeight;
+    const allEntries = ko.utils.unwrapObservable(options.data);
+
+    let visibleEntryCount = 0;
+    let incrementLimit = 0; // The diff required to re-render, set to visibleCount below
+    let elementIncrement = 0; // Elements to add on either side of the visible elements, set to 3x visibleCount
+
+    const updateVisibleEntryCount = function() {
+      // TODO: Drop the window innerHeight limitation.
+      // Sometimes after resizeWrapper() is called the reported innerHeight of the $container is the same as
+      // the wrapper causing the binding to render all the items. This limits the visibleEntryCount to the
+      // window height.
+      const newEntryCount = Math.ceil(
+        Math.min($(window).innerHeight(), $container.innerHeight()) / entryMinHeight
+      );
+      if (newEntryCount !== visibleEntryCount) {
+        const diff = newEntryCount - visibleEntryCount;
+        elementIncrement = options.elementIncrement || 25;
+        incrementLimit = options.incrementLimit || 5;
+        visibleEntryCount = newEntryCount;
+        endIndex += diff;
+        huePubSub.publish('foreach.visible.update', id);
+      }
+    };
+
+    // TODO: Move intervals to webworker
+    const updateCountInterval = setInterval(updateVisibleEntryCount, 300);
+    updateVisibleEntryCount();
+
+    // In case this element was rendered before use the last known indices
+    let startIndex = Math.max($parentFVOwnerElement.data('startIndex') || 0, 0);
+    let endIndex = Math.min(
+      $parentFVOwnerElement.data('endIndex') || visibleEntryCount + elementIncrement,
+      allEntries.length - 1
+    );
+
+    const huePubSubs = [];
+
+    const scrollToIndex = function(idx, offset, instant, callback) {
+      const lastKnownHeights = $parentFVOwnerElement.data('lastKnownHeights');
+      if (!lastKnownHeights || lastKnownHeights.length <= idx) {
+        return;
+      }
+      let top = 0;
+      for (let i = 0; i < idx; i++) {
+        top += lastKnownHeights[i];
+      }
+      const bottom = top + lastKnownHeights[idx];
+      window.setTimeout(() => {
+        const newScrollTop = top + offset;
+        if (instant) {
+          if (newScrollTop >= $container.height() + $container.scrollTop()) {
+            $container.scrollTop(bottom - $container.height());
+          } else if (newScrollTop <= $container.scrollTop()) {
+            $container.scrollTop(newScrollTop);
+          }
+        } else {
+          $container.stop().animate({ scrollTop: newScrollTop }, '500', 'swing', () => {
+            if (callback) {
+              callback();
+            }
+          });
+        }
+      }, 0);
+    };
+
+    if (!options.skipScrollEvent) {
+      huePubSubs.push(
+        huePubSub.subscribe('assist.db.scrollTo', targetEntry => {
+          let foundIndex = -1;
+          $.each(allEntries, (idx, entry) => {
+            if (targetEntry === entry) {
+              foundIndex = idx;
+              return false;
+            }
+          });
+          if (foundIndex !== -1) {
+            const offset = depth > 0 ? $parentFVOwnerElement.position().top : 0;
+            scrollToIndex(foundIndex, offset, false, () => {
+              huePubSub.publish('assist.db.scrollToComplete', targetEntry);
+            });
+          }
+        })
+      );
+    }
+
+    if (ko.isObservable(viewModel.foreachVisible)) {
+      viewModel.foreachVisible({
+        scrollToIndex: function(index) {
+          const offset = depth > 0 ? $parentFVOwnerElement.position().top : 0;
+          scrollToIndex(index, offset, true);
+        }
+      });
+    }
+
+    const childBindingContext = bindingContext.createChildContext(
+      bindingContext.$rawData,
+      null,
+      context => {
+        ko.utils.extend(context, {
+          $parentForeachVisible: $element,
+          $parentForeachVisibleId: id,
+          $depth: depth + 1,
+          $indexOffset: function() {
+            return startIndex;
+          }
+        });
+      }
+    );
+
+    let $wrapper = $element.parent();
+    if (!$wrapper.hasClass('foreach-wrapper')) {
+      $wrapper = $('<div>')
+        .css({
+          position: 'relative',
+          width: '100%'
+        })
+        .addClass('foreach-wrapper')
+        .insertBefore($element);
+      if (options.usePreloadBackground) {
+        $wrapper.addClass('assist-preloader-wrapper');
+        $element.addClass('assist-preloader');
+      }
+      $element
+        .css({
+          position: 'absolute',
+          top: 0,
+          width: '100%'
+        })
+        .appendTo($wrapper);
+    }
+
+    // This is kept up to date with the currently rendered elements, it's used to keep track of any
+    // height changes of the elements.
+    let renderedElements = [];
+
+    if (
+      !$parentFVOwnerElement.data('lastKnownHeights') ||
+      $parentFVOwnerElement.data('lastKnownHeights').length !== allEntries.length
+    ) {
+      const lastKnownHeights = [];
+      $.each(allEntries, () => {
+        lastKnownHeights.push(entryMinHeight);
+      });
+      $parentFVOwnerElement.data('lastKnownHeights', lastKnownHeights);
+    }
+
+    const resizeWrapper = function() {
+      let totalHeight = 0;
+      const lastKnownHeights = $parentFVOwnerElement.data('lastKnownHeights');
+      if (!lastKnownHeights) {
+        return;
+      }
+      $.each(lastKnownHeights, (idx, height) => {
+        totalHeight += height;
+      });
+      $wrapper.height(totalHeight + 'px');
+    };
+    resizeWrapper();
+
+    const updateLastKnownHeights = function() {
+      if ($container.data('busyRendering')) {
+        return;
+      }
+      const lastKnownHeights = $parentFVOwnerElement.data('lastKnownHeights');
+      // Happens when closing first level and the third level is open, disposal tells the parents
+      // to update their heights...
+      if (!lastKnownHeights) {
+        return;
+      }
+      let diff = false;
+      let updateEntryCount = false;
+      $.each(renderedElements, (idx, renderedElement) => {
+        // TODO: Figure out why it goes over index at the end scroll position
+        if (startIndex + idx < lastKnownHeights.length) {
+          const renderedHeight = $(renderedElement).outerHeight(true);
+          if (renderedHeight > 5 && lastKnownHeights[startIndex + idx] !== renderedHeight) {
+            if (renderedHeight < entryMinHeight) {
+              entryMinHeight = renderedHeight;
+              updateEntryCount = true;
+            }
+            lastKnownHeights[startIndex + idx] = renderedHeight;
+            diff = true;
+          }
+        }
+      });
+
+      if (updateEntryCount) {
+        updateVisibleEntryCount();
+      }
+      // Only resize if a difference in height was noticed.
+      if (diff) {
+        $parentFVOwnerElement.data('lastKnownHeights', lastKnownHeights);
+        resizeWrapper();
+      }
+    };
+
+    let updateHeightsInterval = window.setInterval(updateLastKnownHeights, 600);
+
+    huePubSubs.push(
+      huePubSub.subscribe('foreach.visible.update.heights', targetId => {
+        if (targetId === id) {
+          clearInterval(updateHeightsInterval);
+          updateLastKnownHeights();
+          huePubSub.publish('foreach.visible.update.heights', parentId);
+          updateHeightsInterval = window.setInterval(updateLastKnownHeights, 600);
+        }
+      })
+    );
+
+    updateLastKnownHeights();
+
+    const positionList = function() {
+      const lastKnownHeights = $parentFVOwnerElement.data('lastKnownHeights');
+      if (!lastKnownHeights) {
+        return;
+      }
+      let top = 0;
+      for (let i = 0; i < startIndex; i++) {
+        top += lastKnownHeights[i];
+      }
+      $element.css('top', top + 'px');
+    };
+
+    const afterRender = function() {
+      renderedElements = isTable ? $element.children('tbody').children() : $element.children();
+      $container.data('busyRendering', false);
+      if (typeof options.fetchMore !== 'undefined' && endIndex === allEntries.length - 1) {
+        options.fetchMore();
+      }
+      huePubSub.publish('foreach.visible.update.heights', id);
+    };
+
+    const render = function() {
+      if ((endIndex === 0 && allEntries.length > 1) || endIndex < 0) {
+        ko.bindingHandlers.template.update(
+          element,
+          () => {
+            return {
+              foreach: [],
+              templateEngine: ko.nativeTemplateEngine.instance,
+              afterRender: function() {
+                // This is called once for each added element (not when elements are removed)
+                clearTimeout(throttle);
+                throttle = setTimeout(afterRender, 0);
+              }
+            };
+          },
+          allBindings,
+          viewModel,
+          childBindingContext
+        );
+        return;
+      }
+      $container.data('busyRendering', true);
+      // Save the start and end index for when the list is removed and is shown again.
+      $parentFVOwnerElement.data('startIndex', startIndex);
+      $parentFVOwnerElement.data('endIndex', endIndex);
+      positionList();
+
+      // This is to ensure that our afterRender is called (the afterRender of KO below isn't called
+      // when only elements are removed)
+      let throttle = setTimeout(afterRender, 0);
+
+      ko.bindingHandlers.template.update(
+        element,
+        () => {
+          return {
+            foreach: allEntries.slice(startIndex, endIndex + 1),
+            templateEngine: ko.nativeTemplateEngine.instance,
+            afterRender: function() {
+              // This is called once for each added element (not when elements are removed)
+              clearTimeout(throttle);
+              throttle = setTimeout(afterRender, 0);
+            }
+          };
+        },
+        allBindings,
+        viewModel,
+        childBindingContext
+      );
+    };
+
+    const setStartAndEndFromScrollTop = function() {
+      const lastKnownHeights = $parentFVOwnerElement.data('lastKnownHeights');
+      if (!lastKnownHeights) {
+        return;
+      }
+
+      let parentSpace = 0;
+
+      let $lastParent = $parentFVElement;
+      let $lastRef = $element;
+
+      while ($lastParent) {
+        // Include the header, parent() is .foreach-wrapper, parent().parent() is the container (ul or div)
+        const lastRefOffset = $lastRef
+          .parent()
+          .parent()
+          .offset().top;
+        let lastAddedSpace = 0;
+        $lastParent.children().each((idx, child) => {
+          const $child = $(child);
+          if (lastRefOffset > $child.offset().top) {
+            lastAddedSpace = $child.outerHeight(true);
+            parentSpace += lastAddedSpace;
+          } else {
+            // Remove the height of the child which is the parent of this
+            parentSpace -= lastAddedSpace;
+            return false;
+          }
+        });
+        parentSpace += $lastParent.position().top;
+        $lastRef = $lastParent;
+        $lastParent = $lastParent.data('parentForeachVisible');
+      }
+      let position = Math.min($container.scrollTop() - parentSpace, $wrapper.height());
+
+      for (let i = 0; i < lastKnownHeights.length; i++) {
+        position -= lastKnownHeights[i];
+        if (position <= 0) {
+          startIndex = Math.max(i - elementIncrement, 0);
+          endIndex = Math.min(allEntries.length - 1, i + elementIncrement + visibleEntryCount);
+          break;
+        }
+      }
+    };
+
+    let renderThrottle = -1;
+    let preloadGhostThrottle = -1;
+    let lastScrollTop = -1;
+    const onScroll = function() {
+      if (
+        startIndex > incrementLimit &&
+        Math.abs(lastScrollTop - $container.scrollTop()) < incrementLimit * options.minHeight
+      ) {
+        return;
+      }
+      lastScrollTop = $container.scrollTop();
+
+      setStartAndEndFromScrollTop();
+
+      // adds a preload ghost image just on scroll and removes it 200ms after the scroll stops
+      if (options.usePreloadBackground) {
+        $wrapper.addClass('assist-preloader-ghost');
+        clearTimeout(preloadGhostThrottle);
+        preloadGhostThrottle = setTimeout(() => {
+          $wrapper.removeClass('assist-preloader-ghost');
+        }, 200);
+      }
+
+      clearTimeout(renderThrottle);
+      const startDiff = Math.abs($parentFVOwnerElement.data('startIndex') - startIndex);
+      const endDiff = Math.abs($parentFVOwnerElement.data('endIndex') - endIndex);
+      if (
+        startDiff > incrementLimit ||
+        endDiff > incrementLimit ||
+        (startDiff !== 0 && startIndex === 0) ||
+        (endDiff !== 0 && endIndex === allEntries.length - 1)
+      ) {
+        renderThrottle = setTimeout(render, 0);
+      }
+    };
+
+    huePubSubs.push(
+      huePubSub.subscribe('foreach.visible.update', callerId => {
+        if (callerId === id && endIndex > 0) {
+          setStartAndEndFromScrollTop();
+          clearTimeout(renderThrottle);
+          renderThrottle = setTimeout(render, 0);
+        }
+      })
+    );
+
+    $container.bind('scroll', onScroll);
+
+    $parentFVOwnerElement.data('disposalFunction', () => {
+      setTimeout(() => {
+        huePubSub.publish('foreach.visible.update.heights', parentId);
+      }, 0);
+      huePubSubs.forEach(pubSub => {
+        pubSub.remove();
+      });
+      $container.unbind('scroll', onScroll);
+      clearInterval(updateCountInterval);
+      clearInterval(updateHeightsInterval);
+      $parentFVOwnerElement.data('disposalFunction', null);
+    });
+
+    if (typeof options.pubSubDispose !== 'undefined') {
+      huePubSubs.push(
+        huePubSub.subscribe(options.pubSubDispose, $parentFVOwnerElement.data('disposalFunction'))
+      );
+    }
+
+    ko.utils.domNodeDisposal.addDisposeCallback(
+      $wrapper[0],
+      $parentFVOwnerElement.data('disposalFunction')
+    );
+
+    setStartAndEndFromScrollTop();
+    render();
+  }
+};
+
+ko.expressionRewriting.bindingRewriteValidators['foreachVisible'] = false;
+ko.virtualElements.allowedBindings['foreachVisible'] = true;

+ 134 - 0
desktop/core/src/desktop/js/ko/bindings/ko.fresherEditor.js

@@ -0,0 +1,134 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import hueUtils from 'utils/hueUtils';
+
+ko.bindingHandlers.fresherEditor = {
+  init: function(element, valueAccessor) {
+    const _el = $(element);
+    const options = $.extend(valueAccessor(), {});
+    _el.html(options.data());
+    _el.freshereditor({
+      excludes: [
+        'strikethrough',
+        'removeFormat',
+        'insertorderedlist',
+        'justifyfull',
+        'insertheading1',
+        'insertheading2',
+        'superscript',
+        'subscript'
+      ]
+    });
+    _el.freshereditor('edit', true);
+    _el.on('mouseup', () => {
+      storeSelection();
+      updateValues();
+    });
+
+    let sourceDelay = -1;
+    _el.on('keyup', () => {
+      clearTimeout(sourceDelay);
+      storeSelection();
+      sourceDelay = setTimeout(() => {
+        updateValues();
+      }, 100);
+    });
+
+    $('.chosen-select').chosen({
+      disable_search_threshold: 10,
+      width: '75%'
+    });
+
+    $(document).on('addFieldToVisual', (e, field) => {
+      _el.focus();
+      pasteHtmlAtCaret('{{' + field.name() + '}}');
+    });
+
+    $(document).on('addFunctionToVisual', (e, fn) => {
+      _el.focus();
+      pasteHtmlAtCaret(fn);
+    });
+
+    function updateValues() {
+      $('[data-template]')[0].editor.setValue(hueUtils.stripHtmlFromFunctions(_el.html()));
+      valueAccessor().data(_el.html());
+    }
+
+    function storeSelection() {
+      if (window.getSelection) {
+        // IE9 and non-IE
+        const sel = window.getSelection();
+        if (sel.getRangeAt && sel.rangeCount) {
+          const range = sel.getRangeAt(0);
+          _el.data('range', range);
+        }
+      } else if (document.selection && document.selection.type !== 'Control') {
+        // IE < 9
+        _el.data('selection', document.selection);
+      }
+    }
+
+    function pasteHtmlAtCaret(html) {
+      let sel, range;
+      if (window.getSelection) {
+        // IE9 and non-IE
+        sel = window.getSelection();
+        if (sel.getRangeAt && sel.rangeCount) {
+          if (_el.data('range')) {
+            range = _el.data('range');
+          } else {
+            range = sel.getRangeAt(0);
+          }
+          range.deleteContents();
+
+          // Range.createContextualFragment() would be useful here but is
+          // non-standard and not supported in all browsers (IE9, for one)
+          const el = document.createElement('div');
+          el.innerHTML = html;
+          const frag = document.createDocumentFragment();
+          let node, lastNode;
+          while ((node = el.firstChild)) {
+            lastNode = frag.appendChild(node);
+          }
+          range.insertNode(frag);
+
+          // Preserve the selection
+          if (lastNode) {
+            range = range.cloneRange();
+            range.setStartAfter(lastNode);
+            range.collapse(true);
+            sel.removeAllRanges();
+            sel.addRange(range);
+          }
+        }
+      } else if (document.selection && document.selection.type !== 'Control') {
+        // IE < 9
+        if (_el.data('selection')) {
+          _el
+            .data('selection')
+            .createRange()
+            .pasteHTML(html);
+        } else {
+          document.selection.createRange().pasteHTML(html);
+        }
+      }
+    }
+  }
+};

+ 29 - 0
desktop/core/src/desktop/js/ko/bindings/ko.hdfsAutocomplete.js

@@ -0,0 +1,29 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.hdfsAutocomplete = {
+  init: function(element) {
+    const $element = $(element);
+    $element.attr('autocomplete', 'off');
+    $element.jHueHdfsAutocomplete({
+      skipKeydownEvents: true,
+      skipScrollEvent: true
+    });
+  }
+};

+ 38 - 0
desktop/core/src/desktop/js/ko/bindings/ko.hdfsTree.js

@@ -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.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.hdfsTree = {
+  update: function(element, valueAccessor) {
+    const options = ko.unwrap(valueAccessor());
+    const $element = $(element);
+
+    $element.empty();
+
+    $element.jHueHdfsTree({
+      home: '',
+      isS3: !!options.isS3,
+      root: options.root,
+      initialPath: options.path,
+      withTopPadding: false,
+      onPathChange: function(path) {
+        options.selectedPath(path);
+      }
+    });
+  }
+};

+ 139 - 0
desktop/core/src/desktop/js/ko/bindings/ko.highlight.js

@@ -0,0 +1,139 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+// TODO: Depends on Ace
+
+ko.bindingHandlers.highlight = {
+  init: function(element) {
+    $(element).addClass('ace-highlight');
+  },
+  update: function(element, valueAccessor) {
+    const options = $.extend(
+      {
+        dialect: 'hive',
+        value: '',
+        formatted: false
+      },
+      valueAccessor()
+    );
+
+    let value = ko.unwrap(options.value);
+
+    if (typeof value !== 'undefined' && value !== '') {
+      // allows highlighting static code
+      if (options.path) {
+        value = value[options.path];
+      }
+      ace.require(
+        [
+          'ace/mode/impala_highlight_rules',
+          'ace/mode/hive_highlight_rules',
+          'ace/mode/xml_highlight_rules',
+          'ace/tokenizer',
+          'ace/layer/text',
+          'ace/config'
+        ],
+        (impalaRules, hiveRules, xmlRules, tokenizer, text, config) => {
+          const res = [];
+
+          const Tokenizer = tokenizer.Tokenizer;
+          let Rules = hiveRules.HiveHighlightRules;
+          if (options.dialect && ko.unwrap(options.dialect) === 'impala') {
+            Rules = impalaRules.ImpalaHighlightRules;
+          }
+
+          config.loadModule(['theme', $.totalStorage('hue.ace.theme') || 'ace/theme/hue']);
+
+          const Text = text.Text;
+
+          const tok = new Tokenizer(new Rules().getRules());
+          const lines = value.split('\n');
+
+          const renderSimpleLine = function(txt, stringBuilder, tokens) {
+            let screenColumn = 0;
+            let token = tokens[0];
+            let value = token.value;
+            if (value) {
+              screenColumn = txt.$renderToken(stringBuilder, screenColumn, token, value);
+            }
+            for (let i = 1; i < tokens.length; i++) {
+              token = tokens[i];
+              value = token.value;
+              try {
+                screenColumn = txt.$renderToken(stringBuilder, screenColumn, token, value);
+              } catch (e) {
+                if (console && console.warn) {
+                  console.warn(
+                    value,
+                    'This token has some parsing errors and it has been rendered without highlighting.'
+                  );
+                }
+                stringBuilder.push(value);
+                screenColumn = screenColumn + value.length;
+              }
+            }
+          };
+
+          let additionalClass = '';
+          if (!options.splitLines && !options.formatted) {
+            additionalClass = 'pull-left';
+          } else if (options.formatted) {
+            additionalClass = 'ace-highlight-pre';
+          }
+
+          lines.forEach(line => {
+            const renderedTokens = [];
+            const tokens = tok.getLineTokens(line);
+
+            if (tokens && tokens.tokens.length) {
+              renderSimpleLine(
+                new Text(document.createElement('div')),
+                renderedTokens,
+                tokens.tokens
+              );
+            }
+
+            res.push(
+              '<div class="ace_line ' +
+                additionalClass +
+                '">' +
+                renderedTokens.join('') +
+                '&nbsp;</div>'
+            );
+          });
+
+          element.innerHTML =
+            '<div class="ace_editor ace-hue"' +
+            (options.enableOverflow ? ' style="overflow: initial !important;"' : '') +
+            '><div class="ace_layer" style="position: static;' +
+            (options.enableOverflow ? ' overflow: initial !important;' : '') +
+            '">' +
+            res.join('') +
+            '</div></div>';
+          if (options.enableOverflow) {
+            $(element).css({ overflow: 'auto' });
+          }
+          $(element)
+            .find('.ace_invisible_space')
+            .remove();
+        }
+      );
+    }
+  }
+};

+ 101 - 0
desktop/core/src/desktop/js/ko/bindings/ko.hiveChooser.js

@@ -0,0 +1,101 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.hiveChooser = {
+  init: function(element, valueAccessor, allBindingsAccessor) {
+    const self = $(element);
+    let options = ko.unwrap(valueAccessor());
+    let complexConfiguration = false;
+    if (typeof options === 'function') {
+      self.val(options());
+    } else if (options && options.data) {
+      self.val(options.data);
+      complexConfiguration = true;
+    } else {
+      self.val(options);
+    }
+
+    if (complexConfiguration) {
+      self.jHueGenericAutocomplete({
+        showOnFocus: true,
+        skipColumns: ko.unwrap(options.skipColumns),
+        skipTables: ko.unwrap(options.skipTables),
+        startingPath: options.database + '.',
+        rewriteVal: true,
+        onPathChange: options.onChange,
+        namespace: ko.unwrap(options.namespace),
+        compute: ko.unwrap(options.compute),
+        searchEverywhere: ko.unwrap(options.searchEverywhere) || false,
+        apiHelperUser: ko.unwrap(options.apiHelperUser) || '',
+        apiHelperType: ko.unwrap(options.apiHelperType) || '',
+        mainScrollable: ko.unwrap(options.mainScrollable) || $(window)
+      });
+    } else {
+      options = allBindingsAccessor();
+
+      const setPathFromAutocomplete = path => {
+        self.val(path);
+        valueAccessor()(path);
+        self.blur();
+      };
+
+      self.on('blur', () => {
+        if (!options.skipInvalids) {
+          valueAccessor()(self.val());
+        }
+      });
+      if (
+        allBindingsAccessor().valueUpdate != null &&
+        allBindingsAccessor().valueUpdate === 'afterkeydown'
+      ) {
+        self.on('keyup', () => {
+          valueAccessor()(self.val());
+        });
+      }
+      self.jHueGenericAutocomplete({
+        showOnFocus: true,
+        home: '/',
+        skipColumns: ko.unwrap(options.skipColumns) || false,
+        skipTables: ko.unwrap(options.skipTables) || false,
+        namespace: ko.unwrap(options.namespace),
+        compute: ko.unwrap(options.compute),
+        pathChangeLevel: ko.unwrap(options.pathChangeLevel) || '',
+        apiHelperUser: ko.unwrap(options.apiHelperUser) || '',
+        apiHelperType: ko.unwrap(options.apiHelperType) || '',
+        mainScrollable: ko.unwrap(options.mainScrollable) || $(window),
+        onPathChange: function(path) {
+          setPathFromAutocomplete(path);
+        },
+        onEnter: function(el) {
+          if (!options.skipInvalids) {
+            setPathFromAutocomplete(el.val());
+          }
+        },
+        onBlur: function() {
+          if (self.val().lastIndexOf('.') === self.val().length - 1) {
+            self.val(self.val().substr(0, self.val().length - 1));
+          }
+          if (!options.skipInvalids) {
+            valueAccessor()(self.val());
+          }
+        }
+      });
+    }
+  }
+};

+ 46 - 0
desktop/core/src/desktop/js/ko/bindings/ko.html.js

@@ -0,0 +1,46 @@
+// 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 hueUtils from 'utils/hueUtils';
+
+// we override the default html binding to prevent XSS/JS injection
+const originalHtmlBinding = ko.bindingHandlers.html;
+
+ko.bindingHandlers.html = {
+  init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
+    const newValueAccessor = function() {
+      return hueUtils.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()));
+    };
+    originalHtmlBinding.update(element, newValueAccessor, allBindings, viewModel, bindingContext);
+  }
+};
+
+ko.bindingHandlers.htmlUnsecure = {
+  init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
+    originalHtmlBinding.init(element, valueAccessor, allBindings, viewModel, bindingContext);
+  },
+  update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
+    originalHtmlBinding.update(element, valueAccessor, allBindings, viewModel, bindingContext);
+  }
+};

+ 49 - 0
desktop/core/src/desktop/js/ko/bindings/ko.hueCheckAll.js

@@ -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 ko from 'knockout';
+
+ko.bindingHandlers.hueCheckAll = {
+  init: function(element, valueAccessor) {
+    const allValues = ko.utils.unwrapObservable(valueAccessor()).allValues;
+    const selectedValues = ko.utils.unwrapObservable(valueAccessor()).selectedValues;
+
+    const updateCheckedState = function() {
+      ko.utils.toggleDomNodeCssClass(
+        element,
+        'fa-check',
+        allValues().length > 0 && selectedValues().length === allValues().length
+      );
+      ko.utils.toggleDomNodeCssClass(
+        element,
+        'fa-minus hue-uncheck',
+        selectedValues().length > 0 && selectedValues().length !== allValues().length
+      );
+    };
+
+    ko.utils.registerEventHandler(element, 'click', () => {
+      if (selectedValues().length === 0) {
+        selectedValues(allValues().slice(0));
+      } else {
+        selectedValues([]);
+      }
+    });
+
+    selectedValues.subscribe(updateCheckedState);
+    allValues.subscribe(updateCheckedState);
+    updateCheckedState();
+  }
+};

+ 37 - 0
desktop/core/src/desktop/js/ko/bindings/ko.hueCheckbox.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 ko from 'knockout';
+
+ko.bindingHandlers.hueCheckbox = {
+  after: ['value', 'attr'],
+  init: function(element, valueAccessor) {
+    const value = valueAccessor();
+    $(element).addClass('hue-checkbox fa');
+
+    const updateCheckedState = function() {
+      ko.utils.toggleDomNodeCssClass(element, 'fa-check', value());
+    };
+
+    ko.utils.registerEventHandler(element, 'click', () => {
+      value(!value());
+    });
+
+    value.subscribe(updateCheckedState);
+    updateCheckedState();
+  }
+};

+ 44 - 0
desktop/core/src/desktop/js/ko/bindings/ko.hueChecked.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 ko from 'knockout';
+
+ko.bindingHandlers.hueChecked = {
+  after: ['value', 'attr'],
+  init: function(element, valueAccessor, allBindings, viewModel) {
+    const selectedValues = valueAccessor();
+
+    if (allBindings().checkedValue) {
+      viewModel = ko.unwrap(allBindings().checkedValue);
+    }
+
+    const updateCheckedState = function() {
+      ko.utils.toggleDomNodeCssClass(element, 'fa-check', selectedValues.indexOf(viewModel) > -1);
+    };
+
+    ko.utils.registerEventHandler(element, 'click', () => {
+      const currentIndex = selectedValues.indexOf(viewModel);
+      if (currentIndex === -1) {
+        selectedValues.push(viewModel);
+      } else if (currentIndex > -1) {
+        selectedValues.splice(currentIndex, 1);
+      }
+    });
+
+    selectedValues.subscribe(updateCheckedState);
+    updateCheckedState();
+  }
+};

+ 63 - 0
desktop/core/src/desktop/js/ko/bindings/ko.hueLink.js

@@ -0,0 +1,63 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import huePubSub from 'utils/huePubSub';
+
+ko.bindingHandlers.hueLink = {
+  init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
+    ko.bindingHandlers.click.init(
+      element,
+      () => {
+        return function(data, event) {
+          const url = ko.unwrap(valueAccessor());
+          if (url) {
+            let prefix = '';
+            if (window.IS_HUE_4) {
+              prefix = '/hue' + (url.indexOf('/') === 0 ? '' : '/');
+            }
+            if ($(element).attr('target')) {
+              window.open(prefix + url, $(element).attr('target'));
+            } else if (event.ctrlKey || event.metaKey || event.which === 2) {
+              window.open(prefix + url, '_blank');
+            } else {
+              huePubSub.publish('open.link', url);
+            }
+          }
+        };
+      },
+      allBindings,
+      viewModel,
+      bindingContext
+    );
+
+    ko.bindingHandlers.hueLink.update(element, valueAccessor);
+  },
+  update: function(element, valueAccessor) {
+    const url = ko.unwrap(valueAccessor());
+    if (url) {
+      if (window.IS_HUE_4) {
+        $(element).attr('href', '/hue' + (url.indexOf('/') === 0 ? url : '/' + url));
+      } else {
+        $(element).attr('href', url);
+      }
+    } else {
+      $(element).attr('href', 'javascript: void(0);');
+    }
+  }
+};

+ 84 - 0
desktop/core/src/desktop/js/ko/bindings/ko.hueSpinner.js

@@ -0,0 +1,84 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+/**
+ * Binding for adding a spinner to the page
+ *
+ * Example:
+ *
+ * <!-- ko hueSpinner: { spin: loading, center: true, size: 'large' } --><!-- /ko -->
+ *
+ */
+ko.bindingHandlers.hueSpinner = {
+  update: function(element, valueAccessor) {
+    const value = ko.unwrap(valueAccessor());
+
+    const options = {
+      size: 'default',
+      center: false,
+      overlay: false,
+      inline: false,
+      blackout: false
+    };
+
+    let spin = false;
+    if (ko.isObservable(valueAccessor())) {
+      spin = value();
+    } else {
+      $.extend(options, value);
+      spin = typeof value.spin === 'function' ? value.spin() : value.spin;
+    }
+
+    ko.virtualElements.emptyNode(element);
+
+    if (spin) {
+      const $container = $('<div>');
+      $container.addClass(
+        options.overlay
+          ? 'hue-spinner-overlay'
+          : options.inline
+          ? 'hue-spinner-inline'
+          : 'hue-spinner'
+      );
+      if (options.blackout) {
+        $container.addClass('hue-spinner-blackout');
+      }
+      if (!options.overlay) {
+        const $spinner = $('<i>');
+        $spinner.addClass('fa fa-spinner fa-spin');
+        if (options.size === 'large') {
+          $spinner.addClass('hue-spinner-large');
+        }
+        if (options.size === 'xlarge') {
+          $spinner.addClass('hue-spinner-xlarge');
+        }
+        if (options.center) {
+          $spinner.addClass('hue-spinner-center');
+          if (options.inline) {
+            $container.css('width', '100%');
+          }
+        }
+        $container.append($spinner);
+      }
+      ko.virtualElements.prepend(element, $container[0]);
+    }
+  }
+};
+
+ko.virtualElements.allowedBindings.hueSpinner = true;

+ 182 - 0
desktop/core/src/desktop/js/ko/bindings/ko.hueach.js

@@ -0,0 +1,182 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import huePubSub from 'utils/huePubSub';
+
+ko.bindingHandlers.hueach = {
+  init: function(element, valueAccessor, allBindings) {
+    const valueAccessorBuilder = function() {
+      return {
+        data: ko.observableArray([])
+      };
+    };
+    ko.bindingHandlers.foreach.init(element, valueAccessorBuilder, allBindings);
+  },
+  update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
+    const $element = $(element);
+    let $parent = $element.parent();
+    const data =
+      typeof valueAccessor().data === 'function' ? valueAccessor().data() : valueAccessor().data;
+    const considerStretching = valueAccessor().considerStretching || false;
+    const itemHeight = valueAccessor().itemHeight || 22;
+    const scrollable = valueAccessor().scrollable || 'body';
+    const scrollUp = valueAccessor().scrollUp || false;
+    const scrollableOffset = valueAccessor().scrollableOffset || 0;
+    const disableHueEachRowCount = valueAccessor().disableHueEachRowCount || 0;
+    const forceRenderSub = valueAccessor().forceRenderSub || null;
+    let renderTimeout = -1;
+    let dataHasChanged = true;
+
+    let wrappable = $(element);
+    if (data.length > disableHueEachRowCount) {
+      if ($parent.is('table')) {
+        wrappable = $parent;
+        $parent = wrappable.parent();
+      }
+
+      if (!wrappable.parent().hasClass('hueach')) {
+        wrappable.wrap('<div class="hueach"></div>');
+        $parent = wrappable.parent();
+        wrappable.css({
+          position: 'absolute',
+          width: '100%'
+        });
+      }
+
+      $parent.height(data.length * itemHeight);
+      if (wrappable.is('table')) {
+        $parent.height($parent.height() + (data.length > 0 ? itemHeight : 0));
+      }
+    }
+
+    try {
+      if (
+        ko.utils.domData.get(element, 'originalData') &&
+        JSON.stringify(ko.utils.domData.get(element, 'originalData')) === JSON.stringify(data)
+      ) {
+        dataHasChanged = false;
+      }
+    } catch (e) {}
+
+    if (dataHasChanged) {
+      ko.utils.domData.set(element, 'originalData', data);
+    }
+
+    let startItem = 0,
+      endItem = 0;
+    const valueAccessorBuilder = function() {
+      return {
+        data: ko.utils.domData.get(element, 'originalData')
+          ? ko.observableArray(
+              ko.utils.domData.get(element, 'originalData').slice(startItem, endItem)
+            )
+          : []
+      };
+    };
+
+    const render = function() {
+      if ($parent.parents('.hueach').length === 0) {
+        let heightCorrection = 0,
+          fluidCorrection = 0;
+        const scrollTop = $parent.parents(scrollable).scrollTop();
+        if (wrappable.is('table')) {
+          if (scrollTop < scrollableOffset + itemHeight) {
+            wrappable.find('thead').css('opacity', '1');
+          } else {
+            wrappable.find('thead').css('opacity', '0');
+          }
+        } else {
+          wrappable.children(':visible').each((cnt, child) => {
+            if ($(child).height() >= itemHeight) {
+              heightCorrection += $(child).height();
+            }
+          });
+          if (heightCorrection > 0) {
+            ko.utils.domData.set(element, 'heightCorrection', heightCorrection);
+          }
+          if (heightCorrection === 0 && ko.utils.domData.get(element, 'heightCorrection')) {
+            fluidCorrection = ko.utils.domData.get(element, 'heightCorrection') - 20;
+          }
+        }
+        startItem = Math.max(
+          0,
+          Math.floor(
+            Math.max(1, scrollTop - heightCorrection - fluidCorrection - scrollableOffset) /
+              itemHeight
+          ) - 10
+        );
+        if (wrappable.is('table') && startItem % 2 === 1) {
+          startItem--;
+        }
+        endItem = Math.min(
+          startItem + Math.ceil($parent.parents(scrollable).height() / itemHeight) + 20,
+          data.length
+        );
+        wrappable.css('top', startItem * itemHeight + fluidCorrection + 'px');
+      } else {
+        startItem = 0;
+        endItem = data.length;
+      }
+      bindingContext.$indexOffset = function() {
+        return startItem;
+      };
+      ko.bindingHandlers.foreach.update(
+        element,
+        valueAccessorBuilder,
+        allBindings,
+        viewModel,
+        bindingContext
+      );
+    };
+
+    $parent.parents(scrollable).off('scroll');
+    huePubSub.publish('scrollable.scroll.off', scrollable);
+
+    $parent.parents(scrollable).on('scroll', render);
+    if (scrollUp) {
+      $parent.parents(scrollable).jHueScrollUp();
+    }
+
+    if ($parent.parents('.hueach').length > 0) {
+      window.setTimeout(render, 100);
+    }
+
+    if (considerStretching) {
+      huePubSub.subscribe('assist.stretchDown', () => {
+        window.clearTimeout(renderTimeout);
+        renderTimeout = window.setTimeout(() => {
+          ko.utils.domData.set(element, 'hasStretched', true);
+          render();
+        }, 300);
+      });
+    }
+
+    if (forceRenderSub) {
+      huePubSub.subscribe(forceRenderSub, () => {
+        window.setTimeout(render, 300);
+      });
+    }
+
+    window.clearTimeout(renderTimeout);
+    renderTimeout = window.setTimeout(() => {
+      ko.utils.domData.set(element, 'hasStretched', true);
+      render();
+    }, 300);
+  }
+};

+ 48 - 0
desktop/core/src/desktop/js/ko/bindings/ko.impalaDagre.js

@@ -0,0 +1,48 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import huePubSub from 'utils/huePubSub';
+
+// TODO: Depends on impalaDagre
+
+ko.bindingHandlers.impalaDagre = (function() {
+  return {
+    init: function(element) {
+      const id = $(element).attr('id');
+      const self = this;
+      self._impalaDagre = impalaDagre(id);
+      const clickSubscription = huePubSub.subscribe('impala.node.moveto', value => {
+        self._impalaDagre.moveTo(value);
+      });
+      const selectSubscription = huePubSub.subscribe('impala.node.select', value => {
+        self._impalaDagre.select(value);
+      });
+      ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
+        clickSubscription.remove();
+        selectSubscription.remove();
+      });
+    },
+    update: function(element, valueAccessor) {
+      const props = ko.unwrap(valueAccessor());
+      this._impalaDagre.metrics(props.metrics);
+      this._impalaDagre.height(props.height);
+      this._impalaDagre.update(props.value);
+    }
+  };
+})();

+ 24 - 0
desktop/core/src/desktop/js/ko/bindings/ko.jHueRowSelector.js

@@ -0,0 +1,24 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.jHueRowSelector = {
+  init: function(element) {
+    $(element).jHueRowSelector();
+  }
+};

+ 61 - 0
desktop/core/src/desktop/js/ko/bindings/ko.logResizer.js

@@ -0,0 +1,61 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.logResizer = {
+  init: function(element, valueAccessor) {
+    const options = ko.unwrap(valueAccessor()),
+      $resizer = $(element),
+      $parent = $resizer.parents(options.parent),
+      $target = $parent.find(options.target),
+      onStart = options.onStart,
+      onResize = options.onResize;
+
+    const initialHeight = $.totalStorage('hue.editor.logs.size') || 80;
+
+    window.setTimeout(() => {
+      $target.css('height', initialHeight + 'px');
+    }, 0);
+
+    $resizer.draggable({
+      axis: 'y',
+      start: function() {
+        if (onStart) {
+          onStart();
+        }
+      },
+      drag: function(event, ui) {
+        let currentHeight = ui.offset.top - $target.offset().top - 20;
+        if (options.minHeight && currentHeight < options.minHeight) {
+          currentHeight = options.minHeight;
+        }
+        $.totalStorage('hue.editor.logs.size', currentHeight);
+        $target.css('height', currentHeight + 'px');
+        ui.offset.top = 0;
+        ui.position.top = 0;
+      },
+      stop: function(event, ui) {
+        ui.offset.top = 0;
+        ui.position.top = 0;
+        if (onResize) {
+          onResize();
+        }
+      }
+    });
+  }
+};

+ 78 - 0
desktop/core/src/desktop/js/ko/bindings/ko.logScroller.js

@@ -0,0 +1,78 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import hueUtils from 'utils/hueUtils';
+
+ko.bindingHandlers.logScroller = {
+  init: function(element, valueAccessor, allBindings) {
+    const $element = $(element);
+
+    $element.on('scroll', () => {
+      $element.data('lastScrollTop', $element.scrollTop());
+    });
+
+    $element.on('wheel', () => {
+      $element.data('hasUserScrolled', true);
+    });
+
+    function autoLogScroll() {
+      const elementHeight = $element.innerHeight();
+      const lastHeight = $element.data('lastHeight') || elementHeight;
+      const lastScrollTop = $element.data('lastScrollTop') || 0;
+      const hasUserScrolled = $element.data('hasUserScrolled') || false;
+      const lastScrollHeight = $element.data('lastScrollHeight') || elementHeight;
+
+      const stickToBottom =
+        !hasUserScrolled ||
+        elementHeight !== lastHeight ||
+        lastScrollTop + elementHeight === lastScrollHeight;
+
+      if (stickToBottom) {
+        $element.scrollTop(element.scrollHeight - $element.height());
+        $element.data('lastScrollTop', $element.scrollTop());
+      }
+
+      $element.data('lastScrollHeight', element.scrollHeight);
+      $element.data('lastHeight', elementHeight);
+    }
+
+    const logValue = valueAccessor();
+    logValue.subscribe(() => {
+      window.setTimeout(autoLogScroll, 200);
+    });
+
+    if (typeof allBindings().logScrollerVisibilityEvent !== 'undefined') {
+      allBindings().logScrollerVisibilityEvent.subscribe(() => {
+        window.setTimeout(autoLogScroll, 0);
+      });
+    } else {
+      hueUtils.waitForRendered(
+        element,
+        el => {
+          return el.is(':visible');
+        },
+        autoLogScroll,
+        300
+      );
+      ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
+        window.clearTimeout($element.data('waitForRenderTimeout'));
+      });
+    }
+  }
+};

+ 50 - 0
desktop/core/src/desktop/js/ko/bindings/ko.medium.js

@@ -0,0 +1,50 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+// TODO: Depends on MediumEditor
+
+ko.bindingHandlers.medium = {
+  init: function(element, valueAccessor, allBindings) {
+    new MediumEditor($(element), {
+      buttons: [
+        'header1',
+        'header2',
+        'bold',
+        'italic',
+        'underline',
+        'quote',
+        'anchor',
+        'orderedlist',
+        'unorderedlist',
+        'pre',
+        'outdent',
+        'indent'
+      ],
+      buttonLabels: 'fontawesome',
+      anchorTarget: true,
+      anchorInputPlaceholder: '${ _("Paste or type a link") }',
+      anchorInputCheckboxLabel: '${ _("Open in new window") }',
+      firstHeader: 'h2',
+      secondHeader: 'h3'
+    });
+    $(element).on('blur', () => {
+      allBindings().value($(element).html());
+    });
+  }
+};

+ 42 - 0
desktop/core/src/desktop/js/ko/bindings/ko.moment.js

@@ -0,0 +1,42 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.moment = {
+  update: function(element, valueAccessor) {
+    const options = ko.unwrap(valueAccessor());
+    const $element = $(element);
+
+    const value = typeof options.data === 'function' ? options.data() : options.data;
+
+    function render() {
+      const fMoment = moment(value);
+      let text;
+      if (!fMoment.isValid()) {
+        text = value;
+      } else if (options.format) {
+        text = fMoment.format(options.format);
+      } else {
+        text = fMoment.format();
+      }
+      $element.text(text);
+    }
+
+    render();
+  }
+};

+ 41 - 0
desktop/core/src/desktop/js/ko/bindings/ko.momentFromNow.js

@@ -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.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.momentFromNow = {
+  update: function(element, valueAccessor) {
+    const options = ko.unwrap(valueAccessor());
+    const $element = $(element);
+
+    const value = typeof options.data === 'function' ? options.data() : options.data;
+
+    function render() {
+      $element.text(moment(value).fromNow());
+      if (options.titleFormat) {
+        $element.attr('title', moment(value).format(options.titleFormat));
+      }
+    }
+
+    render();
+
+    if (options.interval) {
+      window.clearInterval($element.data('momentInterval'));
+      $element.data('momentInterval', window.setInterval(render, options.interval));
+    }
+  }
+};

+ 64 - 0
desktop/core/src/desktop/js/ko/bindings/ko.multiCheck.js

@@ -0,0 +1,64 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.multiCheck = {
+  init: function(element, valueAccessor) {
+    $(element)
+      .attr('unselectable', 'on')
+      .css('user-select', 'none')
+      .on('selectstart', false);
+
+    const $container = $(ko.unwrap(valueAccessor()));
+    $(element).click(e => {
+      const $self = $(e.target);
+      if ($self.data('noMultiCheck')) {
+        $self.data('noMultiCheck', false);
+        return;
+      }
+      const shouldCheck = $self.is(':checked') || !$self.hasClass('fa-check');
+      if (e.shiftKey && shouldCheck === $container.data('last-clicked-checkbox-state')) {
+        let insideGroup = false;
+        let allCheckboxes = $container.find(':checkbox');
+        if (allCheckboxes.length === 0) {
+          allCheckboxes = $container.find('.hue-checkbox');
+        }
+        for (let i = 0; i < allCheckboxes.length; i++) {
+          const checkbox = allCheckboxes[i];
+          if (checkbox === e.target || checkbox === $container.data('last-clicked-checkbox')) {
+            if (insideGroup) {
+              break;
+            }
+            insideGroup = true;
+            continue;
+          }
+          if (insideGroup) {
+            const $checkbox = $(checkbox);
+            $checkbox.data('noMultiCheck', true);
+            if (($checkbox.is(':checked') || $checkbox.hasClass('fa-check')) !== shouldCheck) {
+              $checkbox.trigger('click');
+            }
+          }
+        }
+      }
+      $container.data('last-clicked-checkbox', e.target);
+      $container.data('last-clicked-checkbox-state', shouldCheck);
+    });
+  },
+  update: function() {}
+};

+ 66 - 0
desktop/core/src/desktop/js/ko/bindings/ko.multiCheckForeachVisible.js

@@ -0,0 +1,66 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.multiCheckForeachVisible = {
+  init: function(element, valueAccessor, allBindings, clickedEntry, bindingContext) {
+    const $element = $(element);
+    const parentContext = bindingContext.$parentContext;
+
+    const selectedAttr = valueAccessor().selectedAttr;
+    const entries = valueAccessor().entries;
+
+    $element
+      .attr('unselectable', 'on')
+      .css('user-select', 'none')
+      .on('selectstart', false);
+
+    $element.on('click', e => {
+      if (e.shiftKey && parentContext.$multiCheckLastEntry) {
+        const lastEntry = parentContext.$multiCheckLastEntry;
+        let inside = false;
+        entries().every(otherEntry => {
+          if (otherEntry === lastEntry || otherEntry === clickedEntry) {
+            if (inside) {
+              return false;
+            }
+            inside = true;
+            return true;
+          }
+          if (inside && otherEntry[selectedAttr]() !== lastEntry[selectedAttr]()) {
+            otherEntry[selectedAttr](lastEntry[selectedAttr]());
+          }
+          return true;
+        });
+        if (clickedEntry[selectedAttr]() !== lastEntry[selectedAttr]()) {
+          clickedEntry[selectedAttr](lastEntry[selectedAttr]());
+        }
+      } else {
+        clickedEntry[selectedAttr](!clickedEntry[selectedAttr]());
+      }
+
+      parentContext.$multiCheckLastEntry = clickedEntry;
+      parentContext.$multiCheckLastChecked = clickedEntry[selectedAttr]();
+    });
+
+    ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
+      $element.off('click');
+    });
+  },
+  update: function() {}
+};

+ 63 - 0
desktop/core/src/desktop/js/ko/bindings/ko.multiClick.js

@@ -0,0 +1,63 @@
+// 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';
+
+ko.bindingHandlers.multiClick = {
+  init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
+    const clickHandlerFunction = valueAccessor().click;
+    const dblClickHandlerFunction = valueAccessor().dblClick;
+    if (!dblClickHandlerFunction && !clickHandlerFunction) {
+      return;
+    }
+
+    let clickedOnce = false;
+    let singleClickTimeout = -1;
+    let dblClickTimeout = -1;
+
+    const newValueAccessor = function() {
+      return function() {
+        const clickArgs = arguments;
+        if (!dblClickHandlerFunction && clickHandlerFunction) {
+          clickHandlerFunction.apply(viewModel, clickArgs);
+          return;
+        }
+        if (clickedOnce) {
+          dblClickHandlerFunction.apply(viewModel, clickArgs);
+          clickedOnce = false;
+          clearTimeout(singleClickTimeout);
+          clearTimeout(dblClickTimeout);
+        } else if (clickHandlerFunction) {
+          clickedOnce = true;
+          singleClickTimeout = window.setTimeout(() => {
+            clickHandlerFunction.apply(viewModel, clickArgs);
+            dblClickTimeout = window.setTimeout(() => {
+              clickedOnce = false;
+            }, 100);
+          }, 225);
+        }
+      };
+    };
+
+    ko.bindingHandlers.click.init(
+      element,
+      newValueAccessor,
+      allBindings,
+      viewModel,
+      bindingContext
+    );
+  }
+};

+ 46 - 0
desktop/core/src/desktop/js/ko/bindings/ko.multiLineEllipsis.js

@@ -0,0 +1,46 @@
+// 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 MultiLineEllipsisHandler from 'utils/multiLineEllipsisHandler';
+
+ko.bindingHandlers.multiLineEllipsis = {
+  after: ['text', 'value'],
+  init: function(element, valueAccessor) {},
+  update: function(element, valueAccessor) {
+    let options = {};
+    if (valueAccessor && ko.isObservable(valueAccessor())) {
+      options.overflowing = valueAccessor();
+    } else if (valueAccessor) {
+      options = valueAccessor() || {};
+    }
+    const multiLineEllipsisHandler = new MultiLineEllipsisHandler({
+      element: element,
+      text: element.textContent,
+      overflowing: options.overflowing,
+      linkify: true,
+      expandable: options.expandable,
+      expanded: options.expanded,
+      expandActionClass: options.expandActionClass,
+      expandClass: options.expandClass
+    });
+
+    ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
+      multiLineEllipsisHandler.dispose();
+    });
+  }
+};

+ 62 - 0
desktop/core/src/desktop/js/ko/bindings/ko.numberFormat.js

@@ -0,0 +1,62 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.numberFormat = (function() {
+  let that;
+  return (that = {
+    init: function(element, valueAccessor) {
+      that.format(element, valueAccessor);
+    },
+    update: function(element, valueAccessor) {
+      that.format(element, valueAccessor);
+    },
+    format: function(element, valueAccessor) {
+      const value = valueAccessor();
+      const unwrapped = ko.unwrap(value);
+      $(element).text(that.human(unwrapped.value, unwrapped.unit));
+    },
+    human: function(value, unit) {
+      let fn;
+      if (unit === 1) {
+        fn = function(value) {
+          return ko.bindingHandlers.simplesize.humanSize(value) + '/s';
+        };
+      } else if (unit === 3) {
+        fn = ko.bindingHandlers.bytesize.humanSize;
+      } else if (unit === 4) {
+        fn = function(value) {
+          return ko.bindingHandlers.bytesize.humanSize(value) + '/s';
+        };
+      } else if (unit === 5) {
+        fn = ko.bindingHandlers.duration.humanTime;
+      } else if (unit === 8) {
+        fn = function(value) {
+          return ko.bindingHandlers.duration.humanTime(value * 1000000);
+        };
+      } else if (unit === 9) {
+        fn = function(value) {
+          return ko.bindingHandlers.duration.humanTime(value * 1000000000);
+        };
+      } else {
+        fn = ko.bindingHandlers.simplesize.humanSize;
+      }
+      return fn(value);
+    }
+  });
+})();

+ 39 - 0
desktop/core/src/desktop/js/ko/bindings/ko.numericTextInput.js

@@ -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 ko from 'knockout';
+
+ko.bindingHandlers.numericTextInput = {
+  init: function(element, valueAccessor, allBindings) {
+    const bindingOptions = ko.unwrap(valueAccessor());
+    const numericValue = ko.observable(bindingOptions.value()).extend({
+      numeric: {
+        precision: bindingOptions.precision,
+        allowEmpty: typeof bindingOptions.allowEmpty !== 'undefined' && bindingOptions.allowEmpty
+      }
+    });
+    numericValue.subscribe(newValue => {
+      bindingOptions.value(newValue);
+    });
+    ko.bindingHandlers.textInput.init(
+      element,
+      () => {
+        return numericValue;
+      },
+      allBindings
+    );
+  }
+};

+ 41 - 0
desktop/core/src/desktop/js/ko/bindings/ko.onClickOutside.js

@@ -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.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.onClickOutside = {
+  update: function(element, valueAccessor, allBindingsAccessor, viewModel) {
+    const options = valueAccessor();
+    const func = typeof options === 'function' ? options : options.onOutside;
+
+    const onDocumentClick = function(event) {
+      if ($.contains(document, event.target) && !$.contains(element, event.target)) {
+        const result = func.bind(viewModel)();
+        if (typeof result === 'undefined' || result) {
+          $(document).off('click', onDocumentClick);
+        }
+      }
+    };
+
+    $(document).off('click', onDocumentClick);
+    $(document).on('click', onDocumentClick);
+
+    ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
+      $(document).off('click', onDocumentClick);
+    });
+  }
+};

+ 34 - 0
desktop/core/src/desktop/js/ko/bindings/ko.oneClickSelect.js

@@ -0,0 +1,34 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.oneClickSelect = {
+  init: function(element) {
+    $(element).click(() => {
+      if (document.selection) {
+        const range = document.body.createTextRange();
+        range.moveToElementText(element);
+        range.select();
+      } else if (window.getSelection) {
+        const range = document.createRange();
+        range.selectNode(element);
+        window.getSelection().addRange(range);
+      }
+    });
+  }
+};

+ 64 - 0
desktop/core/src/desktop/js/ko/bindings/ko.parseArguments.js

@@ -0,0 +1,64 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.parseArguments = {
+  init: function(element, valueAccessor) {
+    const $el = $(element);
+
+    function splitStrings(str) {
+      const bits = [];
+      let isInQuotes = false;
+      let tempStr = '';
+      str
+        .replace(/<\/?arg>|<\/?command>/gi, ' ')
+        .replace(/\r?\n|\r/g, '')
+        .replace(/\s\s+/g, ' ')
+        .split('')
+        .forEach(char => {
+          if (char === '"' || char === "'") {
+            isInQuotes = !isInQuotes;
+          } else if ((char === ' ' || char === '\n') && !isInQuotes && tempStr !== '') {
+            bits.push(tempStr);
+            tempStr = '';
+          } else {
+            tempStr += char;
+          }
+        });
+      if (tempStr !== '') {
+        bits.push(tempStr);
+      }
+      return bits;
+    }
+
+    $el.bind('paste', e => {
+      const pasted = e.originalEvent.clipboardData.getData('text');
+      const args = splitStrings(pasted);
+      if (args.length > 1) {
+        const newList = [];
+        args.forEach(arg => {
+          const obj = {};
+          obj[valueAccessor().objectKey] = $.trim(arg);
+          newList.push(obj);
+        });
+        valueAccessor().list(ko.mapping.fromJS(newList)());
+        valueAccessor().callback();
+      }
+    });
+  }
+};

+ 28 - 0
desktop/core/src/desktop/js/ko/bindings/ko.plotly.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 ko from 'knockout';
+
+// TODO: Depends on Plotly
+
+ko.bindingHandlers.plotly = {
+  init: function(element, valueAccessor) {
+    const options = valueAccessor() || {};
+    if (typeof Plotly !== 'undefined') {
+      Plotly.plot(element, options.data || [], options.layout || {}, { displaylogo: false });
+    }
+  }
+};

+ 43 - 0
desktop/core/src/desktop/js/ko/bindings/ko.publish.js

@@ -0,0 +1,43 @@
+// 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 huePubSub from 'utils/huePubSub';
+
+ko.bindingHandlers.publish = {
+  init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
+    ko.bindingHandlers.click.init(
+      element,
+      () => {
+        return function() {
+          const topicDetails = ko.unwrap(valueAccessor());
+          if (typeof topicDetails === 'string') {
+            huePubSub.publish(topicDetails);
+          } else if (typeof topicDetails === 'object') {
+            const keys = Object.keys(topicDetails);
+            if (keys.length === 1) {
+              huePubSub.publish(keys[0], topicDetails[keys[0]]);
+            }
+          }
+        };
+      },
+      allBindings,
+      viewModel,
+      bindingContext
+    );
+  }
+};

+ 52 - 0
desktop/core/src/desktop/js/ko/bindings/ko.readOnlyAce.js

@@ -0,0 +1,52 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+// TODO: Depends on Ace
+
+ko.bindingHandlers.readOnlyAce = {
+  init: function(element) {
+    $(element).css({
+      'min-height': '250px'
+    });
+    const editor = ace.edit(element);
+    editor.setOptions({
+      readOnly: true,
+      maxLines: Infinity
+    });
+    editor.setTheme($.totalStorage('hue.ace.theme') || 'ace/theme/hue');
+    $(element).data('aceEditor', editor);
+  },
+  update: function(element, valueAccessor, allBindingsAccessor) {
+    let value = ko.unwrap(valueAccessor());
+    const options = ko.unwrap(allBindingsAccessor());
+    $(element)
+      .data('aceEditor')
+      .getSession()
+      .setMode('ace/mode/' + options.type || 'xml'); // e.g. xml, json...
+    if (typeof value !== 'undefined' && value !== '') {
+      // allows highlighting static code
+      if (options.path) {
+        value = value[options.path];
+      }
+      $(element)
+        .data('aceEditor')
+        .setValue(value, -1);
+    }
+  }
+};

+ 33 - 0
desktop/core/src/desktop/js/ko/bindings/ko.resizable.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 $ from 'jquery';
+import ko from 'knockout';
+
+/**
+ * Binding for jquery UI resizable
+ *
+ * @type {{init: ko.bindingHandlers.resizable.init}}
+ */
+ko.bindingHandlers.resizable = {
+  init: function(element, valueAccessor) {
+    const options = valueAccessor() || {};
+    $(element).resizable(options);
+    $(element)
+      .children('.ui-resizable-handle')
+      .css('z-index', 10000);
+  }
+};

+ 228 - 0
desktop/core/src/desktop/js/ko/bindings/ko.select2.js

@@ -0,0 +1,228 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+// TODO: Depends on Role
+
+ko.bindingHandlers.select2 = {
+  init: function(element, valueAccessor, allBindingsAccessor, viewModel) {
+    const options = ko.toJS(valueAccessor()) || {};
+    const $element = $(element);
+
+    // When the options are in the binding value accessor the data attribute will be used instead of any <select>
+    // tag it's attached to.
+    if (ko.isObservable(valueAccessor().options) && ko.isObservable(valueAccessor().value)) {
+      const optionsObservable = valueAccessor().options;
+      const valueObservable = valueAccessor().value;
+      options.data = $.map(optionsObservable(), value => {
+        return { id: value, text: value };
+      });
+      options.val = valueObservable();
+
+      const refreshSelect2Data = function() {
+        $element.select2(
+          'data',
+          $.map(optionsObservable(), value => {
+            return { id: value, text: value };
+          })
+        );
+        $element.select2('val', valueObservable());
+      };
+
+      valueObservable.subscribe(newValue => {
+        if (newValue !== $element.select2('val')) {
+          refreshSelect2Data();
+        }
+      });
+
+      optionsObservable.subscribe(refreshSelect2Data);
+
+      window.setTimeout(() => {
+        refreshSelect2Data();
+      }, 10);
+    }
+
+    if (typeof valueAccessor().vm != 'undefined') {
+      viewModel = valueAccessor().vm;
+    }
+
+    if (typeof valueAccessor().update != 'undefined') {
+      if (
+        options.type === 'user' &&
+        viewModel.selectableHadoopUsers().indexOf(options.update) === -1
+      ) {
+        viewModel.availableHadoopUsers.push({
+          username: options.update
+        });
+      }
+      if (options.type === 'group') {
+        if (options.update instanceof Array) {
+          options.update.forEach(opt => {
+            if (viewModel.selectableHadoopGroups().indexOf(opt) === -1) {
+              viewModel.availableHadoopGroups.push({
+                name: opt
+              });
+            }
+          });
+        } else if (viewModel.selectableHadoopGroups().indexOf(options.update) === -1) {
+          viewModel.availableHadoopGroups.push({
+            name: options.update
+          });
+        }
+      }
+      if (
+        options.type === 'action' &&
+        viewModel.availableActions().indexOf(options.update) === -1
+      ) {
+        viewModel.availableActions.push(options.update);
+      }
+      if (
+        options.type === 'scope' &&
+        viewModel.availablePrivileges().indexOf(options.update) === -1
+      ) {
+        viewModel.availablePrivileges.push(options.update);
+      }
+      if (options.type === 'parameter' && options.update !== '') {
+        let _found = false;
+        allBindingsAccessor()
+          .options()
+          .forEach(opt => {
+            let _option = opt[allBindingsAccessor().optionsValue];
+            if (ko.isObservable(_option)) {
+              _option = _option();
+            }
+            if (_option === options.update) {
+              _found = true;
+            }
+          });
+        if (!_found) {
+          allBindingsAccessor().options.push({
+            name: ko.observable(options.update),
+            value: ko.observable(options.update)
+          });
+        }
+      }
+    }
+    $element
+      .select2(options)
+      .on('change', e => {
+        if (typeof e.val != 'undefined') {
+          if (typeof valueAccessor().update != 'undefined') {
+            valueAccessor().update(e.val);
+          }
+          if (typeof valueAccessor().value != 'undefined') {
+            valueAccessor().value(e.val);
+          }
+        }
+      })
+      .on('select2-focus', () => {
+        if (typeof options.onFocus != 'undefined') {
+          options.onFocus();
+        }
+      })
+      .on('select2-blur', () => {
+        if (typeof options.onBlur != 'undefined') {
+          options.onBlur();
+        }
+      })
+      .on('select2-open', () => {
+        $('.select2-input')
+          .off('keyup')
+          .data('type', options.type)
+          .on('keyup', function(e) {
+            if (e.keyCode === 13) {
+              const _isArray = options.update instanceof Array;
+              const _newVal = $(this).val();
+              const _type = $(this).data('type');
+              if ($.trim(_newVal) !== '') {
+                if (_type === 'user') {
+                  viewModel.availableHadoopUsers.push({
+                    username: _newVal
+                  });
+                }
+                if (_type === 'group') {
+                  viewModel.availableHadoopGroups.push({
+                    name: _newVal
+                  });
+                }
+                if (_type === 'action') {
+                  viewModel.availableActions.push(_newVal);
+                }
+                if (_type === 'scope') {
+                  viewModel.availablePrivileges.push(_newVal);
+                }
+                if (_type === 'role' && window.Role) {
+                  const _r = new Role(viewModel, { name: _newVal });
+                  viewModel.tempRoles.push(_r);
+                  viewModel.roles.push(_r);
+                }
+                if (_type === 'parameter') {
+                  let _found = false;
+                  allBindingsAccessor()
+                    .options()
+                    .forEach(opt => {
+                      if (opt[allBindingsAccessor().optionsValue]() === _newVal) {
+                        _found = true;
+                      }
+                    });
+                  if (!_found) {
+                    allBindingsAccessor().options.push({
+                      name: ko.observable(_newVal),
+                      value: ko.observable(_newVal)
+                    });
+                  }
+                }
+                if (_isArray) {
+                  const _vals = $(element).select2('val');
+                  _vals.push(_newVal);
+                  $(element).select2('val', _vals, true);
+                } else {
+                  $(element).select2('val', _newVal, true);
+                }
+                $(element).select2('close');
+              }
+            }
+          });
+      });
+  },
+  update: function(element, valueAccessor, allBindingsAccessor) {
+    if (typeof allBindingsAccessor().visible != 'undefined') {
+      if (
+        (typeof allBindingsAccessor().visible == 'boolean' && allBindingsAccessor().visible) ||
+        (typeof allBindingsAccessor().visible == 'function' && allBindingsAccessor().visible())
+      ) {
+        $(element)
+          .select2('container')
+          .show();
+      } else {
+        $(element)
+          .select2('container')
+          .hide();
+      }
+    }
+    if (typeof valueAccessor().update != 'undefined') {
+      $(element).select2('val', valueAccessor().update());
+    }
+    if (typeof valueAccessor().readonly != 'undefined') {
+      $(element).select2('readonly', valueAccessor().readonly);
+      if (typeof valueAccessor().readonlySetTo != 'undefined') {
+        valueAccessor().readonlySetTo();
+      }
+    }
+  }
+};

+ 55 - 0
desktop/core/src/desktop/js/ko/bindings/ko.simplesize.js

@@ -0,0 +1,55 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+import sprintf from 'sprintf-js';
+
+ko.bindingHandlers.simplesize = (function() {
+  let that;
+  return (that = {
+    units: ['', 'K', 'M', 'G', 'T', 'P'],
+    init: function(element, valueAccessor) {
+      that.format(element, valueAccessor);
+    },
+    update: function(element, valueAccessor) {
+      that.format(element, valueAccessor);
+    },
+    format: function(element, valueAccessor) {
+      const value = valueAccessor();
+      const formatted = that.humanSize(ko.unwrap(value));
+      $(element).text(formatted);
+    },
+    getBaseLog: function(x, y) {
+      return Math.log(x) / Math.log(y);
+    },
+    humanSize: function(bytes) {
+      const isNumber = !isNaN(parseFloat(bytes)) && isFinite(bytes);
+      if (!isNumber) {
+        return '';
+      }
+
+      // Special case small numbers (including 0), because they're exact.
+      if (bytes < 1000) {
+        return sprintf('%d', bytes);
+      }
+
+      let index = Math.floor(that.getBaseLog(bytes, 1000));
+      index = Math.min(that.units.length - 1, index);
+      return sprintf('%.1f %s', bytes / Math.pow(1000, index), that.units[index]);
+    }
+  });
+})();

+ 32 - 0
desktop/core/src/desktop/js/ko/bindings/ko.slideVisible.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 $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.slideVisible = {
+  init: function(element, valueAccessor) {
+    const value = valueAccessor();
+    $(element).toggle(ko.unwrap(value));
+  },
+  update: function(element, valueAccessor, allBindings) {
+    const value = valueAccessor();
+    const onComplete = ko.unwrap(allBindings()).onComplete;
+    ko.unwrap(value)
+      ? $(element).slideDown(100, onComplete ? onComplete() : () => {})
+      : $(element).slideUp(100, onComplete ? onComplete() : () => {});
+  }
+};

+ 62 - 0
desktop/core/src/desktop/js/ko/bindings/ko.slider.js

@@ -0,0 +1,62 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.slider = {
+  init: function(element, valueAccessor) {
+    const _el = $(element);
+    const _options = $.extend(valueAccessor(), {});
+    _el.slider({
+      min: !isNaN(parseFloat(_options.start())) ? parseFloat(_options.start()) : 0,
+      max: !isNaN(parseFloat(_options.end())) ? parseFloat(_options.end()) : 10,
+      step: !isNaN(parseFloat(_options.gap())) ? parseFloat(_options.gap()) : 1,
+      handle: _options.handle ? _options.handle : 'triangle',
+      start: parseFloat(_options.min()),
+      end: parseFloat(_options.max()),
+      tooltip_split: true,
+      tooltip: 'always',
+      labels: _options.labels
+    });
+    _el.on('slide', e => {
+      _options.start(e.min);
+      _options.end(e.max);
+      _options.min(e.start);
+      _options.max(e.end);
+      if (_options.min() < _options.start()) {
+        _options.start(_options.min());
+      }
+      if (_options.max() > _options.end()) {
+        _options.end(_options.max());
+      }
+      _options.gap(e.step);
+      if (typeof _options.properties.initial_start == 'function') {
+        _options.properties.start(_options.properties.initial_start());
+        _options.properties.end(_options.properties.initial_end());
+        _options.properties.gap(_options.properties.initial_gap());
+      }
+    });
+    _el.on('slideStop', () => {
+      if (window.searchViewModel) {
+        window.searchViewModel.search();
+      }
+    });
+  },
+  update: function(element, valueAccessor) {
+    $.extend(valueAccessor(), {});
+  }
+};

+ 54 - 0
desktop/core/src/desktop/js/ko/bindings/ko.solrChooser.js

@@ -0,0 +1,54 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.solrChooser = {
+  init: function(element, valueAccessor) {
+    const self = $(element);
+    self.val(valueAccessor()());
+
+    function setPathFromAutocomplete(path) {
+      self.val(path);
+      valueAccessor()(path);
+      self.blur();
+    }
+
+    self.on('blur', () => {
+      valueAccessor()(self.val());
+    });
+
+    self.jHueGenericAutocomplete({
+      showOnFocus: true,
+      home: '/',
+      serverType: 'SOLR',
+      skipTables: true, // No notion of DB actually
+      onPathChange: function(path) {
+        setPathFromAutocomplete(path);
+      },
+      onEnter: function(el) {
+        setPathFromAutocomplete(el.val());
+      },
+      onBlur: function() {
+        if (self.val().lastIndexOf('.') === self.val().length - 1) {
+          self.val(self.val().substr(0, self.val().length - 1));
+        }
+        valueAccessor()(self.val());
+      }
+    });
+  }
+};

+ 40 - 0
desktop/core/src/desktop/js/ko/bindings/ko.spinEdit.js

@@ -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.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.spinedit = {
+  init: function(element, valueAccessor, allBindingsAccessor) {
+    const options = $.extend(
+      {
+        minimum: 0,
+        maximum: 10000,
+        step: 5,
+        value: ko.unwrap(valueAccessor()),
+        numberOfDecimals: 0
+      },
+      allBindingsAccessor().override
+    );
+    $(element).spinedit(options);
+    $(element).on('valueChanged', e => {
+      valueAccessor()(e.value);
+    });
+  },
+  update: function(element, valueAccessor) {
+    $(element).spinedit('setValue', ko.unwrap(valueAccessor()));
+  }
+};

+ 187 - 0
desktop/core/src/desktop/js/ko/bindings/ko.splitDraggable.js

@@ -0,0 +1,187 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import huePubSub from 'utils/huePubSub';
+
+ko.bindingHandlers.splitDraggable = {
+  init: function(element, valueAccessor) {
+    const options = ko.unwrap(valueAccessor());
+    let leftPanelWidth =
+      $.totalStorage(options.appName + '_left_panel_width') != null
+        ? Math.max($.totalStorage(options.appName + '_left_panel_width'), 250)
+        : 250;
+    let rightPanelWidth =
+      $.totalStorage(options.appName + '_right_panel_width') != null
+        ? Math.max($.totalStorage(options.appName + '_right_panel_width'), 250)
+        : 290;
+
+    const containerSelector = options.containerSelector || '.panel-container';
+    const contentPanelSelector = options.contentPanelSelector || '.content-panel';
+
+    const onPosition = options.onPosition || function() {};
+
+    const hasLeftPanel = !!options.leftPanelVisible;
+    const hasRightPanel = !!options.rightPanelVisible;
+
+    const isRightPanel = !!options.isRightPanel;
+
+    const $resizer = $(element);
+    const $leftPanel = $('.left-panel');
+    const $rightPanel = $('.right-panel');
+    const $contentPanel = $(contentPanelSelector);
+    const $container = $(containerSelector);
+
+    const positionPanels = function() {
+      if (isRightPanel) {
+        const oppositeWidth =
+          hasLeftPanel && ko.unwrap(options.leftPanelVisible)
+            ? $leftPanel.width() + $resizer.width()
+            : 0;
+        const totalWidth = $container.width() - oppositeWidth;
+        if (ko.unwrap(options.rightPanelVisible) && ko.unwrap(options.rightPanelAvailable)) {
+          $resizer.show();
+          rightPanelWidth = Math.min(rightPanelWidth, $container.width() - 100);
+          const contentPanelWidth = totalWidth - rightPanelWidth - $resizer.width();
+          $rightPanel.css('width', rightPanelWidth + 'px');
+          $contentPanel.css('width', contentPanelWidth + 'px');
+          $resizer.css('left', $container.width() - rightPanelWidth - $resizer.width() + 'px');
+          $contentPanel.css('right', rightPanelWidth + $resizer.width() + 'px');
+        } else {
+          if (oppositeWidth === 0) {
+            $contentPanel.css('width', '100%');
+          } else {
+            $contentPanel.css('width', totalWidth);
+          }
+          $contentPanel.css('right', '0');
+          $resizer.hide();
+        }
+      } else {
+        const oppositeWidth =
+          hasRightPanel &&
+          ko.unwrap(options.rightPanelVisible) &&
+          ko.unwrap(options.rightPanelAvailable)
+            ? $rightPanel.width() + $resizer.width()
+            : 0;
+        const totalWidth = $container.width() - oppositeWidth;
+        if (ko.unwrap(options.leftPanelVisible)) {
+          $resizer.show();
+          leftPanelWidth = Math.min(leftPanelWidth, totalWidth - 100);
+          const contentPanelWidth = totalWidth - leftPanelWidth - $resizer.width();
+          $leftPanel.css('width', leftPanelWidth + 'px');
+          $contentPanel.css('width', contentPanelWidth + 'px');
+          $resizer.css('left', leftPanelWidth + 'px');
+          $contentPanel.css('left', leftPanelWidth + $resizer.width() + 'px');
+        } else {
+          if (oppositeWidth === 0) {
+            $contentPanel.css('width', '100%');
+          } else {
+            $contentPanel.css('width', totalWidth);
+          }
+          $contentPanel.css('left', '0');
+          $resizer.hide();
+        }
+      }
+      onPosition();
+    };
+
+    if (ko.isObservable(options.leftPanelVisible)) {
+      options.leftPanelVisible.subscribe(positionPanels);
+    }
+
+    if (ko.isObservable(options.rightPanelVisible)) {
+      options.rightPanelVisible.subscribe(positionPanels);
+    }
+
+    if (ko.isObservable(options.rightPanelAvailable)) {
+      options.rightPanelAvailable.subscribe(positionPanels);
+    }
+
+    let dragTimeout = -1;
+    $resizer.draggable({
+      axis: 'x',
+      containment: $container,
+      drag: function(event, ui) {
+        if (isRightPanel) {
+          ui.position.left = Math.min($container.width() - 200, ui.position.left);
+        } else {
+          ui.position.left = Math.min(
+            $container.width() - $container.position().left - 200,
+            Math.max(250, ui.position.left)
+          );
+        }
+
+        window.clearTimeout(dragTimeout);
+        dragTimeout = window.setTimeout(() => {
+          if (isRightPanel) {
+            const oppositeWidth =
+              hasLeftPanel && ko.unwrap(options.leftPanelVisible)
+                ? $leftPanel.width() + $resizer.width()
+                : 0;
+            const totalWidth = $container.width() - oppositeWidth;
+            rightPanelWidth = $container.width() - ui.position.left;
+            $rightPanel.css('width', rightPanelWidth + 'px');
+            $contentPanel.css('width', totalWidth - rightPanelWidth + 'px');
+            // $contentPanel.css("right", rightPanelWidth + $resizer.width());
+          } else {
+            const oppositeWidth =
+              hasRightPanel && ko.unwrap(options.rightPanelVisible)
+                ? $rightPanel.width() + $resizer.width()
+                : 0;
+            const totalWidth = $container.width() - oppositeWidth;
+            leftPanelWidth = ui.position.left;
+            $leftPanel.css('width', leftPanelWidth + 'px');
+            $contentPanel.css('width', totalWidth - leftPanelWidth - $resizer.width() + 'px');
+            $contentPanel.css('left', leftPanelWidth + $resizer.width());
+          }
+          onPosition();
+        }, 10);
+      },
+      stop: function() {
+        if (isRightPanel) {
+          $.totalStorage(options.appName + '_right_panel_width', rightPanelWidth);
+        } else {
+          $.totalStorage(options.appName + '_left_panel_width', leftPanelWidth);
+        }
+        window.setTimeout(positionPanels, 100);
+        huePubSub.publish('split.panel.resized');
+      }
+    });
+
+    let positionTimeout = -1;
+    $(window).resize(() => {
+      window.clearTimeout(positionTimeout);
+      positionTimeout = window.setTimeout(() => {
+        positionPanels();
+      }, 1);
+    });
+
+    function initialPositioning() {
+      if (!$container.is(':visible')) {
+        window.setTimeout(initialPositioning, 50);
+      } else {
+        positionPanels();
+        // Even though the container is visible some slow browsers might not
+        // have rendered the panels
+        window.setTimeout(positionPanels, 100);
+      }
+    }
+
+    initialPositioning();
+  }
+};

+ 95 - 0
desktop/core/src/desktop/js/ko/bindings/ko.splitFlexDraggable.js

@@ -0,0 +1,95 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import huePubSub from 'utils/huePubSub';
+
+ko.bindingHandlers.splitFlexDraggable = {
+  init: function(element, valueAccessor) {
+    const options = ko.unwrap(valueAccessor());
+    let sidePanelWidth =
+      $.totalStorage(options.appName + '_' + options.orientation + '_panel_width') != null
+        ? $.totalStorage(options.appName + '_' + options.orientation + '_panel_width')
+        : 290;
+
+    const $resizer = $(element);
+    const $sidePanel = $(options.sidePanelSelector);
+    const $container = $(options.containerSelector);
+
+    const isLeft = options.orientation === 'left';
+
+    const onPosition = options.onPosition || function() {};
+
+    $sidePanel.css('flex-basis', sidePanelWidth + 'px');
+    $resizer.draggable({
+      axis: 'x',
+      containment: $container,
+      start: function() {
+        sidePanelWidth = $sidePanel.width();
+      },
+      drag: function(event, ui) {
+        if (isLeft) {
+          $sidePanel.css('flex-basis', Math.max(sidePanelWidth + ui.position.left, 200) + 'px');
+        } else {
+          $sidePanel.css('flex-basis', Math.max(sidePanelWidth - ui.position.left, 200) + 'px');
+        }
+        onPosition();
+        ui.position.left = 0;
+      },
+      stop: function() {
+        sidePanelWidth = $sidePanel.width();
+        $.totalStorage(
+          options.appName + '_' + options.orientation + '_panel_width',
+          sidePanelWidth
+        );
+        window.setTimeout(positionPanels, 100);
+        huePubSub.publish('split.panel.resized');
+      }
+    });
+
+    const positionPanels = function() {
+      if (options.sidePanelVisible()) {
+        $sidePanel.css('flex-basis', Math.max(sidePanelWidth, 200) + 'px');
+        onPosition();
+      }
+    };
+
+    options.sidePanelVisible.subscribe(positionPanels);
+
+    let positionTimeout = -1;
+    $(window).resize(() => {
+      window.clearTimeout(positionTimeout);
+      positionTimeout = window.setTimeout(() => {
+        positionPanels();
+      }, 1);
+    });
+
+    function initialPositioning() {
+      if (!$container.is(':visible') && !$sidePanel.is(':visible')) {
+        window.setTimeout(initialPositioning, 50);
+      } else {
+        positionPanels();
+        // Even though the container is visible some slow browsers might not
+        // have rendered the panels
+        window.setTimeout(positionPanels, 50);
+      }
+    }
+
+    initialPositioning();
+  }
+};

+ 82 - 0
desktop/core/src/desktop/js/ko/bindings/ko.sqlContextPopover.js

@@ -0,0 +1,82 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import dataCatalog from 'catalog/dataCatalog';
+import huePubSub from 'utils/huePubSub';
+
+/**
+ * Show the Context Popover for SQL or Solr entities when the bound element is clicked.
+ *
+ * Parameters:
+ *
+ * {string} sourceType - 'impala', 'hive' etc.
+ * {ContextNamespace} namespace
+ * {ContextCompute} compute
+ * {string} path - the path, i.e. 'default.customers' or ['default', 'customers'
+ * {string} [orientation] - 'top', 'right', 'bottom', 'left'. Default 'right'
+ * {Object} [offset] - Optional offset from the element
+ * {number} [offset.top] - Offset in pixels
+ * {number} [offset.left] - Offset in pixels
+ *
+ * Examples:
+ *
+ * data-bind="sqlContextPopover: { sourceType: 'impala', namespace: { id: 'myNamespace' }, compute: { id: 'myCompute' }, path: 'default.customers' }"
+ * data-bind="sqlContextPopover: { sourceType: 'hive', namespace: { id: 'myNamespace' }, compute: { id: 'myCompute' }, path: 'default', orientation: 'bottom', offset: { top: 5 } }"
+ *
+ * @type {{init: ko.bindingHandlers.sqlContextPopover.init}}
+ */
+ko.bindingHandlers.sqlContextPopover = {
+  init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
+    ko.bindingHandlers.click.init(
+      element,
+      () => {
+        return function() {
+          const options = valueAccessor();
+          dataCatalog.getEntry(options).done(entry => {
+            const $source = $(element);
+            const offset = $source.offset();
+            if (options.offset) {
+              offset.top += options.offset.top || 0;
+              offset.left += options.offset.left || 0;
+            }
+
+            huePubSub.publish('context.popover.show', {
+              data: {
+                type: 'catalogEntry',
+                catalogEntry: entry
+              },
+              showInAssistEnabled: true,
+              orientation: options.orientation || 'right',
+              source: {
+                element: element,
+                left: offset.left,
+                top: offset.top,
+                right: offset.left + $source.width(),
+                bottom: offset.top + $source.height()
+              }
+            });
+          });
+        };
+      },
+      allBindings,
+      viewModel,
+      bindingContext
+    );
+  }
+};

+ 81 - 0
desktop/core/src/desktop/js/ko/bindings/ko.storageContextPopover.js

@@ -0,0 +1,81 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import AssistStorageEntry from 'assist/assistStorageEntry';
+import huePubSub from 'utils/huePubSub';
+
+/**
+ * Show the Context Popover for files (HDFS, S3, ADLS, ...) when the bound element is clicked.
+ *
+ * Parameters:
+ *
+ * {string} path - the path (can include type, i.e. 'hdfs://tmp'
+ * {string} [type] - Optional type, 'hdfs', 's3' etc. Default 'hdfs'.
+ * {string} [orientation] - 'top', 'right', 'bottom', 'left'. Default 'right';
+ * {Object} [offset] - Optional offset from the element
+ * {number} [offset.top] - Offset in pixels
+ * {number} [offset.left] - Offset in pixels
+ *
+ * Examples:
+ *
+ * data-bind="storageContextPopover: { path: '/tmp/banana.csv' }"
+ * data-bind="storageContextPopover: { path: 's3:/tmp/banana.csv', orientation: 'bottom', offset: { top: 5 } }"
+ *
+ * @type {{init: ko.bindingHandlers.storageContextPopover.init}}
+ */
+ko.bindingHandlers.storageContextPopover = {
+  init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
+    ko.bindingHandlers.click.init(
+      element,
+      () => {
+        return function() {
+          const options = valueAccessor();
+          AssistStorageEntry.getEntry(options.path, options.type).done(entry => {
+            const $source = $(element);
+            const offset = $source.offset();
+
+            if (options.offset) {
+              offset.top += options.offset.top || 0;
+              offset.left += options.offset.left || 0;
+            }
+
+            entry.open(true);
+            huePubSub.publish('context.popover.show', {
+              data: {
+                type: 'storageEntry',
+                storageEntry: entry
+              },
+              orientation: options.orientation || 'right',
+              source: {
+                element: element,
+                left: offset.left,
+                top: offset.top,
+                right: offset.left + $source.width(),
+                bottom: offset.top + $source.height()
+              }
+            });
+          });
+        };
+      },
+      allBindings,
+      viewModel,
+      bindingContext
+    );
+  }
+};

+ 50 - 0
desktop/core/src/desktop/js/ko/bindings/ko.stretchDown.js

@@ -0,0 +1,50 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import huePubSub from 'utils/huePubSub';
+
+ko.bindingHandlers.stretchDown = {
+  init: function(element) {
+    const $element = $(element);
+    const $parent = $element.parent();
+
+    let lastParentHeight = -1;
+    let lastTop = -1;
+
+    function stretch(force) {
+      if (
+        lastParentHeight !== $parent.innerHeight() ||
+        lastTop !== $element.position().top ||
+        force
+      ) {
+        lastParentHeight = $parent.innerHeight();
+        lastTop = $element.position().top;
+        $element.height(
+          lastParentHeight - lastTop - ($element.outerHeight(true) - $element.innerHeight())
+        );
+        huePubSub.publish('assist.stretchDown', $element);
+      }
+    }
+
+    window.setInterval(stretch, 200);
+    huePubSub.subscribe('assist.forceStretchDown', () => {
+      stretch(true);
+    });
+  }
+};

+ 261 - 0
desktop/core/src/desktop/js/ko/bindings/ko.tagEditor.js

@@ -0,0 +1,261 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.tagEditor = {
+  init: function(element, valueAccessor) {
+    let options = valueAccessor();
+    const $element = $(element);
+
+    const validRegExp = options.validRegExp ? new RegExp(options.validRegExp) : undefined;
+
+    const showValidationError = function() {
+      const $errorWrapper = $element.siblings('.selectize-error');
+      if (options.invalidMessage && $errorWrapper.length > 0) {
+        $errorWrapper.find('.message').text(options.invalidMessage);
+        $errorWrapper.show();
+        window.setTimeout(() => {
+          $errorWrapper.fadeOut(400, () => {
+            $errorWrapper.hide();
+          });
+        }, 5000);
+      }
+    };
+
+    options = $.extend(
+      {
+        plugins: ['remove_button'],
+        options: $.map(options.setTags(), value => {
+          return { value: value, text: value };
+        }),
+        delimiter: ',',
+        items: options.setTags(),
+        closeAfterSelect: true,
+        persist: true,
+        preload: true,
+        create: function(input) {
+          if (typeof validRegExp !== 'undefined' && !validRegExp.test(input)) {
+            showValidationError();
+            return false;
+          }
+
+          return {
+            value: input.replace(/\s/g, '-'),
+            text: input.replace(/\s/g, '-')
+          };
+        }
+      },
+      options
+    );
+
+    const $readOnlyContainer = $('<div>')
+      .hide()
+      .addClass('selectize-control selectize-read-only multi')
+      .attr('style', $element.attr('style'))
+      .insertAfter($(element));
+
+    if (!options.readOnly) {
+      $readOnlyContainer.on('mouseover', () => {
+        $readOnlyContainer.find('.selectize-actions').addClass('selectize-actions-visible');
+      });
+
+      $readOnlyContainer.on('mouseout', () => {
+        $readOnlyContainer.find('.selectize-actions').removeClass('selectize-actions-visible');
+      });
+    }
+
+    $element.hide();
+
+    let currentSelectize;
+    let optionsBeforeEdit = [];
+
+    const saveOnClickOutside = function(event) {
+      if (
+        $.contains(document, event.target) &&
+        currentSelectize &&
+        !$.contains(currentSelectize.$wrapper[0], event.target)
+      ) {
+        if (currentSelectize.getValue() !== optionsBeforeEdit.join(',')) {
+          options.onSave(currentSelectize.getValue());
+        }
+        $(document).off('click', saveOnClickOutside);
+        $(document).off('keyup', hideOnEscape);
+        showReadOnly();
+      }
+    };
+
+    const hideOnEscape = function(event) {
+      if (event.which === 27) {
+        showReadOnly();
+      }
+    };
+
+    let sizeCheckInterval = -1;
+
+    ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
+      window.clearInterval(sizeCheckInterval);
+    });
+
+    const showEdit = function() {
+      window.clearInterval(sizeCheckInterval);
+      optionsBeforeEdit = options.setTags().concat();
+      options.options = $.map(options.setTags(), value => {
+        return { value: value, text: value };
+      });
+      currentSelectize = $element.selectize(options)[0].selectize;
+      $readOnlyContainer.hide();
+      $element
+        .next()
+        .find('.selectize-input')
+        .css('padding-right', '38px');
+      $element
+        .next()
+        .find('input')
+        .focus();
+      const $editActions = $('<div>')
+        .addClass('selectize-actions')
+        .appendTo($element.next());
+      $('<i>')
+        .addClass('fa fa-check')
+        .click(() => {
+          if (currentSelectize.getValue() !== optionsBeforeEdit.join(',')) {
+            options.onSave(currentSelectize.getValue());
+          }
+          showReadOnly();
+        })
+        .appendTo($editActions);
+      $('<i>')
+        .addClass('fa fa-close')
+        .click(() => {
+          showReadOnly();
+        })
+        .appendTo($editActions);
+      window.setTimeout(() => {
+        $(document).on('click', saveOnClickOutside);
+        $(document).on('keyup', hideOnEscape);
+      }, 0);
+    };
+
+    let lastKnownOffsetWidth = -1;
+    let lastKnownOffsetHeight = -1;
+
+    const addReadOnlyTagsTillOverflow = function($readOnlyInner) {
+      $readOnlyInner.empty();
+      const tagElements = [];
+      options.setTags().forEach(tag => {
+        tagElements.push(
+          $('<div>')
+            .text(tag)
+            .appendTo($readOnlyInner)
+        );
+      });
+
+      if (!options.readOnly && !options.hasErrors()) {
+        $('<i>')
+          .addClass('fa fa-pencil selectize-edit pointer')
+          .attr('title', HUE_I18n.selectize.editTags)
+          .appendTo($readOnlyInner);
+        $readOnlyInner.click(() => {
+          showEdit();
+        });
+      }
+
+      if (!options.overflowEllipsis) {
+        return;
+      }
+
+      if (
+        $readOnlyInner[0].offsetHeight < $readOnlyInner[0].scrollHeight ||
+        ($readOnlyInner[0].offsetWidth < $readOnlyInner[0].scrollWidth && tagElements.length)
+      ) {
+        tagElements[tagElements.length - 1].after(
+          $('<div>')
+            .addClass('hue-tag-overflow')
+            .text('...')
+        );
+      }
+
+      while (
+        tagElements.length &&
+        ($readOnlyInner[0].offsetHeight < $readOnlyInner[0].scrollHeight ||
+          $readOnlyInner[0].offsetWidth < $readOnlyInner[0].scrollWidth)
+      ) {
+        tagElements.pop().remove();
+      }
+
+      lastKnownOffsetWidth = $readOnlyInner[0].offsetWidth;
+      lastKnownOffsetHeight = $readOnlyInner[0].offsetHeight;
+    };
+
+    const showReadOnly = function() {
+      window.clearInterval(sizeCheckInterval);
+      $(document).off('click', saveOnClickOutside);
+      $(document).off('keyup', hideOnEscape);
+      if (currentSelectize) {
+        currentSelectize.destroy();
+        $element.hide();
+        $element.val(options.setTags().join(','));
+      }
+      $readOnlyContainer.empty();
+      const $readOnlyInner = $('<div>')
+        .addClass('selectize-input items not-full has-options has-items')
+        .appendTo($readOnlyContainer);
+      if (options.setTags().length > 0) {
+        addReadOnlyTagsTillOverflow($readOnlyInner);
+        if (options.overflowEllipsis) {
+          sizeCheckInterval = window.setInterval(() => {
+            if (
+              $readOnlyInner[0].offsetWidth !== lastKnownOffsetWidth ||
+              $readOnlyInner[0].offsetHeight !== lastKnownOffsetHeight
+            ) {
+              addReadOnlyTagsTillOverflow($readOnlyInner);
+            }
+          }, 500);
+        }
+      } else {
+        if (options.hasErrors()) {
+          $('<span>')
+            .addClass('selectize-no-tags')
+            .text(options.errorMessage)
+            .appendTo($readOnlyInner);
+        } else {
+          $('<span>')
+            .addClass('selectize-no-tags')
+            .text(options.emptyPlaceholder)
+            .appendTo($readOnlyInner);
+        }
+
+        if (!options.readOnly && !options.hasErrors()) {
+          $('<i>')
+            .addClass('fa fa-pencil selectize-edit pointer')
+            .attr('title', HUE_I18n.selectize.editTags)
+            .appendTo($readOnlyInner);
+          $readOnlyInner.click(() => {
+            showEdit();
+          });
+        }
+      }
+
+      $readOnlyContainer.attr('title', options.setTags().join(', '));
+
+      $readOnlyContainer.show();
+    };
+
+    showReadOnly();
+  }
+};

+ 35 - 0
desktop/core/src/desktop/js/ko/bindings/ko.tagsNotAllowed.js

@@ -0,0 +1,35 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.tagsNotAllowed = {
+  update: function(element, valueAccessor, allBindingsAccessor) {
+    const $element = $(element);
+    const params = allBindingsAccessor();
+    const valueObservable = ko.isObservable(params)
+      ? params
+      : params.textInput
+      ? params.textInput
+      : params.value;
+    const value = valueObservable();
+    const escaped = value.replace(/<|>/g, '');
+    if (escaped !== value) {
+      $element.val(escaped);
+    }
+  }
+};

+ 97 - 0
desktop/core/src/desktop/js/ko/bindings/ko.templateContextMenu.js

@@ -0,0 +1,97 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.templateContextMenu = {
+  init: function(element, valueAccessor, allBindings, viewModel) {
+    const options = valueAccessor();
+
+    element.addEventListener('contextmenu', event => {
+      if (document.selection && document.selection.empty) {
+        document.selection.empty();
+      } else if (window.getSelection) {
+        const sel = window.getSelection();
+        sel.removeAllRanges();
+      }
+
+      if (typeof options.beforeOpen === 'function') {
+        options.beforeOpen.bind(viewModel)();
+      }
+      let $menu = $('#hueContextMenu_' + options.template);
+      if ($menu.length === 0) {
+        $menu = $(
+          '<ul id="hueContextMenu_' +
+            options.template +
+            '" class="hue-context-menu" data-bind="template: { name: \'' +
+            options.template +
+            '\', data: viewModel, afterRender: afterRender }"></ul>'
+        ).appendTo(window.HUE_CONTAINER);
+      } else {
+        ko.cleanNode($menu[0]);
+      }
+      $menu.data('active', true);
+
+      $menu.css('top', 0);
+      $menu.css('left', 0);
+      $menu.css('opacity', 0);
+      $menu.show();
+
+      const hideMenu = function() {
+        if (!$menu.data('active')) {
+          $menu.hide();
+          ko.cleanNode($menu[0]);
+        }
+      };
+
+      ko.applyBindings(
+        {
+          afterRender: function() {
+            const menuWidth = $menu.outerWidth(true);
+            const menuHeight = $menu.outerHeight(true);
+            $menu.css(
+              'left',
+              event.clientX + menuWidth > $(window).width()
+                ? $(window).width() - menuWidth
+                : event.clientX
+            );
+            $menu.css(
+              'top',
+              event.clientY + menuHeight > $(window).height()
+                ? $(window).height() - menuHeight
+                : event.clientY
+            );
+            $menu.css('opacity', 1);
+            if (options.scrollContainer) {
+              $(options.scrollContainer).one('scroll', hideMenu);
+            }
+            window.setTimeout(() => {
+              $menu.data('active', false);
+              $(document).one('click', hideMenu);
+            }, 100);
+          },
+          viewModel: options.viewModel || viewModel
+        },
+        $menu[0]
+      );
+
+      ko.contextFor($menu[0]).$contextSourceElement = element;
+      event.preventDefault();
+      event.stopPropagation();
+    });
+  }
+};

+ 209 - 0
desktop/core/src/desktop/js/ko/bindings/ko.templatePopover.js

@@ -0,0 +1,209 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+import huePubSub from 'utils/huePubSub';
+
+ko.bindingHandlers.templatePopover = {
+  init: function(element, valueAccessor, allBindingsAccessor, viewModel) {
+    const options = ko.unwrap(valueAccessor());
+
+    const clickTrigger = options.trigger === 'click';
+    let $container = $('#popover-container');
+    if (!$container.length) {
+      $container = $('<div>')
+        .attr('id', 'popover-container')
+        .appendTo(window.HUE_CONTAINER);
+      $('<div>')
+        .addClass('temp-content')
+        .hide()
+        .appendTo($container);
+      $('<div>')
+        .addClass('temp-title')
+        .hide()
+        .appendTo($container);
+    }
+
+    const $content = $container.find('.temp-content');
+    const $title = $container.find('.temp-title');
+
+    $.extend(options, { html: true, trigger: 'manual', container: '#popover-container' });
+
+    const $element = $(element);
+
+    const visible = options.visible || ko.observable(false);
+
+    let trackElementInterval = -1;
+
+    const hidePopover = function() {
+      if (visible()) {
+        window.clearInterval(trackElementInterval);
+        $element.popover('hide');
+        visible(false);
+        $(document).off('click', hideOnClickOutside);
+      }
+    };
+
+    const closeSub = huePubSub.subscribe('close.popover', hidePopover);
+
+    const hideOnClickOutside = function(event) {
+      if (
+        visible() &&
+        $element.data('popover') &&
+        !$.contains($element.data('popover').$tip[0], event.target)
+      ) {
+        hidePopover();
+      }
+    };
+
+    ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
+      if (visible && $element.data('popover')) {
+        hidePopover();
+      }
+      closeSub.remove();
+    });
+
+    const afterRender = function() {
+      options.content = $content.html();
+      options.title = $title.html();
+      let triggerTitle;
+      if ($element.attr('title')) {
+        triggerTitle = $element.attr('title');
+        $element.attr('title', null);
+      }
+      $element.popover(options);
+      $element.popover('show');
+      if (triggerTitle) {
+        $element.attr('title', triggerTitle);
+      }
+      const $tip = $element.data('popover').$tip;
+      if (window.HUE_CONTAINER !== 'body') {
+        $tip.css({ position: 'fixed', 'z-index': 2000 });
+        $tip.appendTo(window.HUE_CONTAINER);
+
+        $tip.offset({
+          left: $element.offset().left + $element.outerWidth(true) + 10,
+          top: $element.offset().top + $element.outerHeight(true) / 2 - $tip.outerHeight(true) / 2
+        });
+      }
+      ko.cleanNode($tip.get(0));
+      ko.applyBindings(viewModel, $tip.get(0));
+      $tip.find('.close-popover, .close-template-popover').click(event => {
+        hidePopover();
+        event.stopPropagation();
+      });
+      if (options.minWidth) {
+        const heightBefore = $tip.outerHeight(true);
+        const widthBefore = $tip.outerWidth(true);
+        $tip.css('min-width', options.minWidth);
+        // The min-width might affect the height/width in which case we reposition the popover depending on placement
+        const heightDiff = (heightBefore - $tip.outerHeight(true)) / 2;
+        const widthDiff = (widthBefore - $tip.outerWidth(true)) / 2;
+        if (
+          (!options.placement || options.placement === 'left' || options.placement === 'right') &&
+          heightDiff !== 0
+        ) {
+          $tip.css('top', $tip.position().top + heightDiff + 'px');
+        } else if (
+          options.placement &&
+          (options.placement === 'bottom' || options.placement === 'top') &&
+          widthDiff !== 0
+        ) {
+          $tip.css('left', $tip.position().left + widthDiff + 'px');
+        }
+      }
+      let lastWidth = $element.outerWidth(true);
+      let lastOffset = $element.offset();
+      let lastHeight = $element.outerHeight(true);
+      trackElementInterval = window.setInterval(() => {
+        const elementWidth = $element.outerWidth(true);
+        const elementHeight = $element.outerHeight(true);
+        const elementOffset = $element.offset();
+        if (
+          lastHeight !== elementHeight ||
+          lastWidth !== $element.outerWidth(true) ||
+          lastOffset.top !== elementOffset.top ||
+          lastOffset.left !== elementOffset.left
+        ) {
+          $tip.css({
+            left: elementOffset.left + elementWidth / 2 - $tip.outerWidth(true) / 2,
+            top: elementOffset.top + elementHeight + 10
+          });
+          lastWidth = elementWidth;
+          lastOffset = elementOffset;
+          lastHeight = elementHeight;
+        }
+      }, 50);
+      $content.empty();
+      $title.empty();
+      $(document).on('click', hideOnClickOutside);
+      visible(true);
+    };
+
+    const showPopover = function(preventClose) {
+      if (!preventClose) {
+        huePubSub.publish('close.popover');
+      }
+      ko.renderTemplate(
+        options.contentTemplate,
+        viewModel,
+        {
+          afterRender: function() {
+            if (options.titleTemplate) {
+              ko.renderTemplate(
+                options.titleTemplate,
+                viewModel,
+                {
+                  afterRender: function() {
+                    afterRender();
+                  }
+                },
+                $title.get(0),
+                'replaceChildren'
+              );
+            } else {
+              afterRender();
+            }
+          }
+        },
+        $content.get(0),
+        'replaceChildren'
+      );
+    };
+
+    if (visible()) {
+      window.setTimeout(() => {
+        showPopover(true);
+      }, 0);
+    }
+
+    if (clickTrigger) {
+      $element.click(e => {
+        if (visible()) {
+          hidePopover();
+        } else {
+          showPopover();
+        }
+        e.stopPropagation();
+      });
+    } else {
+      $element.mouseenter(showPopover);
+      $element.mouseleave(hidePopover);
+    }
+  }
+};

+ 35 - 0
desktop/core/src/desktop/js/ko/bindings/ko.textSqueezer.js

@@ -0,0 +1,35 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.textSqueezer = {
+  init: function(element, valueAccessor) {
+    const value = valueAccessor();
+    const $element = $(element);
+    $element.text(ko.unwrap(value));
+    $element.textSqueezer({
+      decimalPrecision: 2
+    });
+  },
+  update: function(element, valueAccessor) {
+    const value = valueAccessor();
+    const $element = $(element);
+    $element.text(ko.unwrap(value));
+    $element.trigger('redraw');
+  }
+};

+ 29 - 0
desktop/core/src/desktop/js/ko/bindings/ko.timepicker.js

@@ -0,0 +1,29 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import $ from 'jquery';
+import ko from 'knockout';
+
+ko.bindingHandlers.timepicker = {
+  init: function(element) {
+    $(element).timepicker({
+      minuteStep: 1,
+      showSeconds: false,
+      showMeridian: false,
+      defaultTime: false
+    });
+  }
+};

+ 43 - 0
desktop/core/src/desktop/js/ko/bindings/ko.toggle.js

@@ -0,0 +1,43 @@
+// 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';
+
+/**
+ * This binding can be used to toggle a boolean value on click
+ *
+ * Example:
+ *
+ * <div databind="toggle: value">...</div>
+ *
+ * @type {{init: ko.bindingHandlers.toggle.init}}
+ */
+ko.bindingHandlers.toggle = {
+  init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
+    const value = valueAccessor();
+    ko.bindingHandlers.click.init(
+      element,
+      () => {
+        return function() {
+          value(!value());
+        };
+      },
+      allBindings,
+      viewModel,
+      bindingContext
+    );
+  }
+};

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