Browse Source

HUE-8687 [editor] Split notebook.ko.js and move js code blocks from editor_components into webpack

Johan Ahlen 7 years ago
parent
commit
279be3f66e

+ 1482 - 0
desktop/core/src/desktop/js/apps/notebook/app.js

@@ -0,0 +1,1482 @@
+// 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 Clipboard from 'clipboard';
+import 'jquery-mousewheel';
+import 'ext/bootstrap-datepicker.min';
+import 'ext/jquery.hotkeys';
+import 'jquery/plugins/jquery.hdfstree';
+
+import huePubSub from 'utils/huePubSub';
+import hueUtils from 'utils/hueUtils';
+
+window.Clipboard = Clipboard;
+
+const HUE_PUB_SUB_EDITOR_ID =
+  window.location.pathname.indexOf('notebook') > -1 ? 'notebook' : 'editor';
+
+window.MAIN_SCROLLABLE = '.page-content';
+
+let isLeftNavOpen = false;
+huePubSub.subscribe(
+  'left.nav.open.toggle',
+  val => {
+    isLeftNavOpen = val;
+  },
+  HUE_PUB_SUB_EDITOR_ID
+);
+
+huePubSub.subscribe(
+  'split.panel.resized',
+  () => {
+    huePubSub.publish('recalculate.name.description.width');
+  },
+  HUE_PUB_SUB_EDITOR_ID
+);
+
+const showHoverMsg = e => {
+  let dt = null;
+  if (e) {
+    dt = e.dataTransfer;
+  }
+  if (
+    !isLeftNavOpen &&
+    (!dt ||
+      (dt.types &&
+        (dt.types.indexOf ? dt.types.indexOf('Files') !== -1 : dt.types.contains('Files'))))
+  ) {
+    $(window.EDITOR_BINDABLE_ELEMENT)
+      .find('.hoverMsg')
+      .removeClass('hide');
+  }
+};
+
+const hideHoverMsg = vm => {
+  if (vm.editorMode()) {
+    $(window.EDITOR_BINDABLE_ELEMENT)
+      .find('.hoverText')
+      .html(window.HUE_I18n.notebook.dropEditorHoverText);
+  } else {
+    $(window.EDITOR_BINDABLE_ELEMENT)
+      .find('.hoverText')
+      .html(window.HUE_I18n.notebook.dropNotebookHoverText);
+  }
+  $(window.EDITOR_BINDABLE_ELEMENT)
+    .find('.hoverMsg')
+    .addClass('hide');
+};
+
+const createHueDatatable = (el, snippet, vm) => {
+  let DATATABLES_MAX_HEIGHT = 330;
+  let invisibleRows = 10;
+  if (snippet.result && snippet.result.data() && snippet.result.data().length) {
+    const cols = snippet.result.data()[0].length;
+    invisibleRows = cols > 200 ? 10 : cols > 30 ? 50 : 100;
+  }
+  const _dt = $(el).hueDataTable({
+    i18n: {
+      NO_RESULTS: window.HUE_I18n.notebook.noResults,
+      OF: window.HUE_I18n.notebook.of
+    },
+    fnDrawCallback: function(oSettings) {
+      if (vm.editorMode()) {
+        $('#queryResults').removeAttr('style');
+        DATATABLES_MAX_HEIGHT =
+          $(window).height() -
+          $(el)
+            .parent()
+            .offset().top -
+          40;
+        $(el)
+          .parents('.dataTables_wrapper')
+          .css('overflow-x', 'hidden');
+        $(el).jHueHorizontalScrollbar();
+        $(el)
+          .parents('.dataTables_wrapper')
+          .jHueScrollLeft();
+      } else if ($(el).data('fnDraws') === 1) {
+        $(el)
+          .parents('.dataTables_wrapper')
+          .jHueTableScroller({
+            maxHeight: DATATABLES_MAX_HEIGHT,
+            heightAfterCorrection: 0
+          });
+      }
+    },
+    scrollable:
+      vm.editorMode() && !vm.isPresentationMode() ? window.MAIN_SCROLLABLE : '.dataTables_wrapper',
+    contained: !vm.editorMode() || vm.isPresentationMode(),
+    forceInvisible: invisibleRows
+  });
+
+  window.setTimeout(() => {
+    if (vm.editorMode()) {
+      $(el)
+        .parents('.dataTables_wrapper')
+        .css('overflow-x', 'hidden');
+      const bannerTopHeight = window.BANNER_TOP_HTML ? 30 : 2;
+      $(el).jHueTableExtender2({
+        mainScrollable: window.MAIN_SCROLLABLE,
+        fixedFirstColumn: vm.editorMode(),
+        stickToTopPosition: 48 + bannerTopHeight,
+        parentId: 'snippet_' + snippet.id(),
+        clonedContainerPosition: 'fixed',
+        app: 'editor'
+      });
+      $(el).jHueHorizontalScrollbar();
+    } else {
+      $(el).jHueTableExtender2({
+        mainScrollable: $(el).parents('.dataTables_wrapper')[0],
+        fixedFirstColumn: vm.editorMode(),
+        parentId: 'snippet_' + snippet.id(),
+        clonedContainerPosition: 'absolute',
+        app: 'editor'
+      });
+    }
+  }, 0);
+
+  return _dt;
+};
+
+const createDatatable = (el, snippet, vm) => {
+  const parent = $(el).parent();
+  // When executing few columns -> many columns -> few columns we have to clear the style
+  $(el).removeAttr('style');
+  if ($(el).hasClass('table-huedatatable')) {
+    $(el).removeClass('table-huedatatable');
+    if (parent.hasClass('dataTables_wrapper')) {
+      $(el).unwrap();
+    }
+  }
+  $(el).addClass('dt');
+
+  const _dt = createHueDatatable(el, snippet, vm);
+
+  const dataTableEl = $(el).parents('.dataTables_wrapper');
+
+  if (!vm.editorMode()) {
+    dataTableEl.bind('mousewheel DOMMouseScroll wheel', function(e) {
+      if (
+        $(el)
+          .closest('.results')
+          .css('overflow') === 'hidden'
+      ) {
+        return;
+      }
+      const _e = e.originalEvent,
+        _deltaX = _e.wheelDeltaX || -_e.deltaX,
+        _deltaY = _e.wheelDeltaY || -_e.deltaY;
+      this.scrollTop += -_deltaY / 2;
+      this.scrollLeft += -_deltaX / 2;
+
+      if (this.scrollTop === 0) {
+        $('body')[0].scrollTop += -_deltaY / 3;
+        $('html')[0].scrollTop += -_deltaY / 3; // for firefox
+      }
+      e.preventDefault();
+    });
+  }
+
+  let _scrollTimeout = -1;
+
+  let scrollElement = dataTableEl;
+  if (vm.editorMode()) {
+    scrollElement = $(window.MAIN_SCROLLABLE);
+  }
+
+  if (scrollElement.data('scrollFnDtCreation')) {
+    scrollElement.off('scroll', scrollElement.data('scrollFnDtCreation'));
+  }
+
+  let resultFollowTimeout = -1;
+  const dataScroll = function() {
+    if (vm.editorMode()) {
+      const snippetEl = $('#snippet_' + snippet.id());
+      if (snippetEl.find('.dataTables_wrapper').length > 0 && snippet.showGrid()) {
+        window.clearTimeout(resultFollowTimeout);
+        resultFollowTimeout = window.setTimeout(() => {
+          const topCoord = vm.isPresentationMode() || vm.isResultFullScreenMode() ? 50 : 73;
+          let offsetTop = 0;
+          if (
+            snippetEl.find('.dataTables_wrapper').length > 0 &&
+            snippetEl.find('.dataTables_wrapper').offset()
+          ) {
+            offsetTop = (snippetEl.find('.dataTables_wrapper').offset().top - topCoord) * -1;
+          }
+          let margin = Math.max(offsetTop, 0);
+          if (window.BANNER_TOP_HTML) {
+            margin += 31;
+          }
+          if (snippet.isResultSettingsVisible()) {
+            snippetEl.find('.snippet-grid-settings').css({
+              height:
+                vm.isPresentationMode() || !vm.editorMode()
+                  ? '330px'
+                  : Math.max(
+                      100,
+                      Math.ceil(
+                        $(window).height() - Math.max($('#queryResults').offset().top, topCoord)
+                      )
+                    ) + 'px'
+            });
+            snippetEl.find('.result-settings').css({
+              marginTop: margin
+            });
+          }
+          snippetEl.find('.snippet-actions').css({
+            marginTop: margin + 25
+          });
+        }, 100);
+      }
+    }
+    if (
+      !vm.editorMode() ||
+      (vm.editorMode() && snippet.currentQueryTab() === 'queryResults' && snippet.showGrid())
+    ) {
+      let _lastScrollPosition =
+        scrollElement.data('scrollPosition') != null ? scrollElement.data('scrollPosition') : 0;
+      window.clearTimeout(_scrollTimeout);
+      scrollElement.data('scrollPosition', scrollElement.scrollTop());
+      _scrollTimeout = window.setTimeout(() => {
+        if (vm.editorMode()) {
+          _lastScrollPosition--; //hack for forcing fetching
+        }
+        if (
+          _lastScrollPosition !== scrollElement.scrollTop() &&
+          scrollElement.scrollTop() + scrollElement.outerHeight() + 20 >=
+            scrollElement[0].scrollHeight &&
+          _dt &&
+          snippet.result.hasMore()
+        ) {
+          huePubSub.publish('editor.snippet.result.gray', snippet);
+          snippet.fetchResult(100, false);
+        }
+      }, 100);
+    }
+  };
+  scrollElement.data('scrollFnDtCreation', dataScroll);
+  scrollElement.on('scroll', dataScroll);
+  snippet.isResultSettingsVisible.subscribe(newValue => {
+    if (newValue) {
+      dataScroll();
+    }
+  });
+
+  huePubSub.subscribeOnce('chart.hard.reset', () => {
+    // hard reset once the default opened chart
+    const oldChartX = snippet.chartX();
+    snippet.chartX(null);
+    window.setTimeout(() => {
+      snippet.chartX(oldChartX);
+    }, 0);
+  });
+
+  return _dt;
+};
+
+if (ko.options) {
+  ko.options.deferUpdates = true;
+}
+
+let viewModel;
+
+const hideFixedHeaders = function() {
+  $('.jHueTableExtenderClonedContainer').hide();
+  $('.jHueTableExtenderClonedContainerColumn').hide();
+  $('.jHueTableExtenderClonedContainerCell').hide();
+  $('.fixed-header-row').hide();
+  $('.fixed-first-cell').hide();
+  $('.fixed-first-column').hide();
+};
+
+window.hideFixedHeaders = hideFixedHeaders;
+
+let redrawTimeout = -1;
+const redrawFixedHeaders = function(timeout) {
+  const renderer = function() {
+    if (!viewModel.selectedNotebook()) {
+      return;
+    }
+    viewModel
+      .selectedNotebook()
+      .snippets()
+      .forEach(snippet => {
+        if (snippet.result.meta().length > 0) {
+          const tableExtender = $('#snippet_' + snippet.id() + ' .resultTable').data(
+            'plugin_jHueTableExtender2'
+          );
+          if (typeof tableExtender !== 'undefined') {
+            tableExtender.repositionHeader();
+            tableExtender.drawLockedRows();
+          }
+          $(window.MAIN_SCROLLABLE).data('lastScroll', $(window.MAIN_SCROLLABLE).scrollTop());
+          $(window.MAIN_SCROLLABLE).trigger('scroll');
+        }
+      });
+    $('.jHueTableExtenderClonedContainer').show();
+    $('.jHueTableExtenderClonedContainerColumn').show();
+    $('.jHueTableExtenderClonedContainerCell').show();
+    $('.fixed-header-row').show();
+    $('.fixed-first-cell').show();
+    $('.fixed-first-column').show();
+  };
+
+  if (timeout) {
+    window.clearTimeout(redrawTimeout);
+    redrawTimeout = window.setTimeout(renderer, timeout);
+  } else {
+    renderer();
+  }
+};
+window.redrawFixedHeaders = redrawFixedHeaders;
+
+function replaceAce(content) {
+  const snip = viewModel.selectedNotebook().snippets()[0];
+  if (snip) {
+    snip.statement_raw(content);
+    snip.result.statements_count(1);
+    snip.ace().setValue(content, 1);
+    snip.result.statement_range({
+      start: {
+        row: 0,
+        column: 0
+      },
+      end: {
+        row: 0,
+        column: 0
+      }
+    });
+    snip.ace()._emit('focus');
+  }
+  hideHoverMsg(viewModel);
+  redrawFixedHeaders(200);
+}
+window.replaceAce = replaceAce;
+
+// Drag and drop iPython / Zeppelin notebooks
+if (window.FileReader) {
+  let aceChecks = 0;
+
+  const addAce = (content, snippetType) => {
+    const snip = viewModel.selectedNotebook().addSnippet({ type: snippetType, result: {} }, true);
+    snip.statement_raw(content);
+    aceChecks++;
+    snip.checkForAce = window.setInterval(() => {
+      if (snip.ace()) {
+        window.clearInterval(snip.checkForAce);
+        aceChecks--;
+        if (aceChecks === 0) {
+          hideHoverMsg(viewModel);
+          redrawFixedHeaders(200);
+        }
+      }
+    }, 100);
+  };
+
+  const addMarkdown = content => {
+    const snip = viewModel.selectedNotebook().addSnippet({ type: 'markdown', result: {} }, true);
+    snip.statement_raw(content);
+  };
+
+  const addPySpark = content => {
+    addAce(content, 'pyspark');
+  };
+
+  const addSql = content => {
+    addAce(content, 'hive');
+  };
+
+  const addScala = content => {
+    addAce(content, 'spark');
+  };
+
+  const parseExternalJSON = raw => {
+    try {
+      if (viewModel.editorMode()) {
+        replaceAce(raw);
+      } else {
+        const loaded = typeof raw == 'string' ? JSON.parse(raw) : raw;
+        if (loaded.nbformat) {
+          //ipython
+          let cells = [];
+          if (loaded.nbformat === 3) {
+            cells = loaded.worksheets[0].cells;
+          } else if (loaded.nbformat === 4) {
+            cells = loaded.cells;
+          }
+          cells.forEach((cell, cellCnt) => {
+            window.setTimeout(() => {
+              if (cell.cell_type === 'code') {
+                if (loaded.nbformat === 3) {
+                  addPySpark($.isArray(cell.input) ? cell.input.join('') : cell.input);
+                } else {
+                  addPySpark($.isArray(cell.source) ? cell.source.join('') : cell.source);
+                }
+              }
+              if (cell.cell_type === 'heading') {
+                let heading = $.isArray(cell.source) ? cell.source.join('') : cell.source;
+                if (cell.level === 1) {
+                  heading += '\n====================';
+                } else if (cell.level === 2) {
+                  heading += '\n--------------------';
+                } else {
+                  heading = '### ' + heading;
+                }
+                addMarkdown(heading);
+              }
+              if (cell.cell_type === 'markdown') {
+                addMarkdown($.isArray(cell.source) ? cell.source.join('') : cell.source);
+              }
+              if (cellCnt === cells.length - 1 && aceChecks === 0) {
+                hideHoverMsg(viewModel);
+              }
+            }, 10);
+          });
+        }
+
+        if (loaded.paragraphs) {
+          //zeppelin
+          if (loaded.name) {
+            viewModel.selectedNotebook().name(loaded.name);
+          }
+          loaded.paragraphs.forEach(paragraph => {
+            if (paragraph.text) {
+              const content = paragraph.text.split('\n');
+              if (content[0].indexOf('%md') > -1) {
+                content.shift();
+                addMarkdown(content.join('\n'));
+              } else if (content[0].indexOf('%sql') > -1 || content[0].indexOf('%hive') > -1) {
+                content.shift();
+                addSql(content.join('\n'));
+              } else if (content[0].indexOf('%pyspark') > -1) {
+                content.shift();
+                addPySpark(content.join('\n'));
+              } else {
+                if (content[0].indexOf('%spark') > -1) {
+                  content.shift();
+                }
+                addScala(content.join('\n'));
+              }
+            }
+          });
+        }
+      }
+    } catch (e) {
+      hideHoverMsg(viewModel);
+      replaceAce(raw);
+    }
+  };
+
+  const handleFileSelect = function(evt) {
+    evt.stopPropagation();
+    evt.preventDefault();
+    const dt = evt.dataTransfer;
+    const files = dt.files;
+    if (files.length > 0) {
+      showHoverMsg();
+    } else {
+      hideHoverMsg(viewModel);
+    }
+
+    for (let i = 0, f; (f = files[i]); i++) {
+      const reader = new FileReader();
+      reader.onload = (function(file) {
+        return function(e) {
+          $('.hoverText').html("<i class='fa fa-spinner fa-spin'></i>");
+          parseExternalJSON(e.target.result);
+        };
+      })(f);
+      reader.readAsText(f);
+    }
+  };
+
+  const handleDragOver = function(evt) {
+    evt.stopPropagation();
+    evt.preventDefault();
+    evt.dataTransfer.dropEffect = 'copy';
+  };
+
+  const dropZone = $(window.EDITOR_BINDABLE_ELEMENT)[0];
+  dropZone.addEventListener('dragenter', showHoverMsg, false);
+  dropZone.addEventListener('dragover', handleDragOver, false);
+  dropZone.addEventListener('drop', handleFileSelect, false);
+
+  let isDraggingOverText = false;
+
+  $(window.EDITOR_BINDABLE_ELEMENT)
+    .find('.hoverText')
+    .on('dragenter', e => {
+      e.preventDefault();
+      e.stopPropagation();
+      e.stopImmediatePropagation();
+      isDraggingOverText = true;
+    });
+
+  $(window.EDITOR_BINDABLE_ELEMENT)
+    .find('.hoverText')
+    .on('dragleave', e => {
+      e.preventDefault();
+      e.stopPropagation();
+      e.stopImmediatePropagation();
+      isDraggingOverText = false;
+    });
+
+  $(window.EDITOR_BINDABLE_ELEMENT)
+    .find('.hoverMsg')
+    .on('dragleave', e => {
+      if (!isDraggingOverText) {
+        hideHoverMsg(viewModel);
+      }
+    });
+}
+
+$(document).ready(() => {
+  if (window.EDITOR_ENABLE_QUERY_SCHEDULING) {
+    viewModel = new window.EditorViewModel(
+      window.EDITOR_ID,
+      window.NOTEBOOKS_JSON,
+      window.EDITOR_VIEW_MODEL_OPTIONS,
+      window.CoordinatorEditorViewModel,
+      window.RunningCoordinatorModel
+    );
+  } else {
+    viewModel = new window.EditorViewModel(
+      window.EDITOR_ID,
+      window.NOTEBOOKS_JSON,
+      window.EDITOR_VIEW_MODEL_OPTIONS
+    );
+  }
+  ko.applyBindings(viewModel, $(window.EDITOR_BINDABLE_ELEMENT)[0]);
+  viewModel.init();
+
+  window.sqlWorkerHandler.registerWorkers();
+
+  viewModel.selectedNotebook.subscribe(newVal => {
+    huePubSub.publish('selected.notebook.changed', newVal);
+  });
+
+  let wasResultFullScreenMode = false;
+  let isAssistAvailable = viewModel.assistAvailable();
+  let wasLeftPanelVisible = viewModel.isLeftPanelVisible();
+  let wasRightPanelVisible = viewModel.isRightPanelVisible();
+
+  const exitPlayerMode = () => {
+    if (!wasResultFullScreenMode) {
+      viewModel.selectedNotebook().isPresentationMode(false);
+    } else {
+      viewModel.isResultFullScreenMode(false);
+    }
+    wasResultFullScreenMode = false;
+  };
+
+  viewModel.isResultFullScreenMode.subscribe(newValue => {
+    wasResultFullScreenMode = newValue;
+    huePubSub.publish('editor.presentation.operate.toggle', newValue);
+  });
+
+  viewModel.isLeftPanelVisible.subscribe(value => {
+    redrawFixedHeaders(200);
+  });
+
+  // Close the notebook snippets when leaving the page
+  window.onbeforeunload = function(e) {
+    if (!viewModel.selectedNotebook().avoidClosing) {
+      viewModel.selectedNotebook().close();
+    }
+  };
+  $(window).data('beforeunload', window.onbeforeunload);
+
+  $('.preview-sample').css('right', 10 + hueUtils.scrollbarWidth() + 'px');
+
+  const saveKeyHandler = () => {
+    if (viewModel.canSave()) {
+      viewModel.saveNotebook();
+    } else {
+      $('#saveAsModal' + window.EDITOR_SUFFIX).modal('show');
+    }
+  };
+
+  const newKeyHandler = () => {
+    if (!viewModel.editorMode()) {
+      viewModel.selectedNotebook().newSnippet();
+    } else {
+      viewModel.newNotebook(
+        viewModel.editorType(),
+        null,
+        viewModel.selectedNotebook()
+          ? viewModel
+              .selectedNotebook()
+              .snippets()[0]
+              .currentQueryTab()
+          : null
+      );
+    }
+  };
+
+  const initKeydownBindings = () => {
+    $(window).bind('keydown.editor', 'ctrl+s alt+s meta+s', e => {
+      e.preventDefault();
+      saveKeyHandler();
+      return false;
+    });
+    $(window).bind('keydown.editor', 'ctrl+shift+p alt+shift+p meta+shift+p', e => {
+      e.preventDefault();
+      huePubSub.publish('editor.presentation.toggle');
+      return false;
+    });
+    $(window).bind('keydown.editor', 'ctrl+e alt+e meta+e', e => {
+      e.preventDefault();
+      newKeyHandler();
+      return false;
+    });
+  };
+
+  if (document.location.href.indexOf('editor') >= 0) {
+    initKeydownBindings();
+  }
+
+  $(document).bind('keyup', e => {
+    if (
+      e.keyCode === 191 &&
+      e.shiftKey &&
+      !$(e.target).is('input') &&
+      !$(e.target).is('textarea')
+    ) {
+      $('#helpModal' + window.EDITOR_SUFFIX).modal('show');
+    }
+
+    if (
+      e.keyCode === 191 &&
+      !e.shiftKey &&
+      !$(e.target).is('input') &&
+      !$(e.target).is('textarea')
+    ) {
+      if (
+        viewModel.editorMode() &&
+        viewModel
+          .selectedNotebook()
+          .snippets()[0]
+          .currentQueryTab() === 'queryResults'
+      ) {
+        e.preventDefault();
+        const $t = $(
+          '#snippet_' +
+            viewModel
+              .selectedNotebook()
+              .snippets()[0]
+              .id()
+        ).find('.resultTable');
+        $t.hueDataTable().fnShowSearch();
+        return false;
+      }
+    }
+  });
+
+  let initialResizePosition = 100;
+
+  const draggableHelper = (el, e, ui, setSize) => {
+    const _snippet = ko.dataFor(el.parents('.snippet')[0]);
+    const _cm = $('#snippet_' + _snippet.id()).data('editor');
+    const _newSize = _snippet.aceSize() + (ui.offset.top - initialResizePosition);
+    _cm.setSize('99%', _newSize);
+    if (setSize) {
+      _snippet.aceSize(_newSize);
+    }
+  };
+
+  const getDraggableOptions = minY => {
+    return {
+      axis: 'y',
+      start: function(e, ui) {
+        initialResizePosition = ui.offset.top;
+      },
+      drag: function(e, ui) {
+        draggableHelper($(this), e, ui);
+        $('.jHueTableExtenderClonedContainer').hide();
+        $('.jHueTableExtenderClonedContainerColumn').hide();
+        $('.jHueTableExtenderClonedContainerCell').hide();
+        $('.fixed-header-row').hide();
+        $('.fixed-first-cell').hide();
+        $('.fixed-first-column').hide();
+      },
+      stop: function(e, ui) {
+        $('.jHueTableExtenderClonedContainer').show();
+        $('.jHueTableExtenderClonedContainerColum').show();
+        $('.jHueTableExtenderClonedContainerCell').show();
+        $('.fixed-header-row').show();
+        $('.fixed-first-cell').show();
+        $('.fixed-first-column').show();
+        draggableHelper($(this), e, ui, true);
+        redrawFixedHeaders();
+        ui.helper.first().removeAttr('style');
+      },
+      containment: [0, minY, 4000, minY + 400]
+    };
+  };
+
+  $('.resize-panel a').each(function() {
+    $(this).draggable(
+      getDraggableOptions(
+        $(this)
+          .parents('.snippet')
+          .offset().top + 128
+      )
+    );
+  });
+
+  const resetResultsResizer = snippet => {
+    const $snippet = $('#snippet_' + snippet.id());
+    $snippet
+      .find('.table-results .column-side')
+      .width(hueUtils.bootstrapRatios.span3() + '%')
+      .data('newWidth', hueUtils.bootstrapRatios.span3());
+    if (snippet.isResultSettingsVisible()) {
+      $snippet
+        .find('.table-results .grid-side')
+        .data('newWidth', hueUtils.bootstrapRatios.span9())
+        .width(hueUtils.bootstrapRatios.span9() + '%');
+    } else {
+      $snippet
+        .find('.table-results .grid-side')
+        .data('newWidth', 100)
+        .width('100%');
+    }
+    $snippet.find('.resize-bar').css('left', '');
+    try {
+      $snippet.find('.resize-bar').draggable('destroy');
+    } catch (e) {}
+
+    let initialPosition = 0;
+
+    $snippet.find('.resize-bar').draggable({
+      axis: 'x',
+      containment: $snippet.find('.table-results'),
+      create: function() {
+        const $snip = $('#snippet_' + snippet.id());
+        initialPosition = $snip.find('.resize-bar').position().left;
+        $snip
+          .find('.table-results .column-side')
+          .data('newWidth', hueUtils.bootstrapRatios.span3());
+        $snip.find('.meta-filter').width($snip.find('.table-results .column-side').width() - 28);
+      },
+      drag: function(event, ui) {
+        const $snip = $('#snippet_' + snippet.id());
+        if (initialPosition === 0) {
+          initialPosition = $snip.find('.resize-bar').position().left;
+        }
+        ui.position.left = Math.max(150, ui.position.left);
+        const newSpan3Width =
+          (ui.position.left * hueUtils.bootstrapRatios.span3()) / initialPosition;
+        const newSpan9Width = 100 - newSpan3Width - hueUtils.bootstrapRatios.margin();
+        $snip
+          .find('.table-results .column-side')
+          .width(newSpan3Width + '%')
+          .data('newWidth', newSpan3Width);
+        $snip
+          .find('.table-results .grid-side')
+          .width(newSpan9Width + '%')
+          .data('newWidth', newSpan9Width);
+        $snip.find('.meta-filter').width($snip.find('.table-results .column-side').width() - 28);
+      },
+      stop: function() {
+        redrawFixedHeaders();
+        huePubSub.publish('resize.leaflet.map');
+      }
+    });
+  };
+
+  const resizeToggleResultSettings = (snippet, initial) => {
+    let _dtElement;
+    const $snip = $('#snippet_' + snippet.id());
+    if (snippet.showGrid()) {
+      _dtElement = $snip.find('.dataTables_wrapper');
+      const topCoord =
+        viewModel.isPresentationMode() || viewModel.isResultFullScreenMode()
+          ? window.BANNER_TOP_HTML
+            ? 31
+            : 1
+          : 73;
+      $snip.find('.snippet-grid-settings').css({
+        height:
+          viewModel.isPresentationMode() || !viewModel.editorMode()
+            ? '330px'
+            : Math.ceil(
+                $(window).height() -
+                  Math.max(
+                    $('.result-settings').length > 0 ? $('.result-settings').offset().top : 0,
+                    topCoord
+                  )
+              ) + 'px'
+      });
+    } else {
+      _dtElement = $snip.find('.chart:visible');
+    }
+    if (_dtElement.length === 0) {
+      _dtElement = $snip.find('.table-results');
+    }
+    _dtElement
+      .parents('.snippet-body')
+      .find('.toggle-result-settings')
+      .css({
+        height: _dtElement.height() - 30 + 'px',
+        'line-height': _dtElement.height() - 30 + 'px'
+      });
+    if (initial) {
+      $snip.find('.result-settings').css({
+        marginTop: 0
+      });
+      $snip.find('.snippet-actions').css({
+        marginTop: 0
+      });
+      huePubSub.publish('resize.leaflet.map');
+    }
+  };
+
+  const forceChartDraws = initial => {
+    if (viewModel.selectedNotebook()) {
+      viewModel
+        .selectedNotebook()
+        .snippets()
+        .forEach(snippet => {
+          if (snippet.result.data().length > 0) {
+            let _elCheckerInterval = -1;
+            const _el = $('#snippet_' + snippet.id());
+            _elCheckerInterval = window.setInterval(() => {
+              if (_el.find('.resultTable').length > 0) {
+                try {
+                  resizeToggleResultSettings(snippet, initial);
+                  resetResultsResizer(snippet);
+                  $(document).trigger('forceChartDraw', snippet);
+                } catch (e) {}
+                window.clearInterval(_elCheckerInterval);
+              }
+            }, 200);
+          }
+        });
+    }
+  };
+
+  forceChartDraws(true);
+
+  // ======== PubSub ========
+
+  let splitDraggableTimeout = -1;
+  huePubSub.subscribe(
+    'split.draggable.position',
+    () => {
+      window.clearTimeout(splitDraggableTimeout);
+      splitDraggableTimeout = window.setTimeout(() => {
+        redrawFixedHeaders(100);
+      }, 200);
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe(
+    'redraw.fixed.headers',
+    () => {
+      hideFixedHeaders();
+      redrawFixedHeaders(200);
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe(
+    'app.gained.focus',
+    app => {
+      if (app === 'editor') {
+        huePubSub.publish('redraw.fixed.headers');
+        huePubSub.publish('hue.scrollleft.show');
+        huePubSub.publish('active.snippet.type.changed', viewModel.editorType());
+      }
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe(
+    'current.query.tab.switched',
+    tab => {
+      if (tab !== 'queryResults') {
+        $('.hue-datatable-search').hide();
+      }
+      if (tab === 'queryHistory') {
+        hueUtils.waitForRendered(
+          $('#queryHistory .history-table'),
+          el => {
+            return el.is(':visible');
+          },
+          () => {
+            viewModel.selectedNotebook().forceHistoryInitialHeight(true);
+            huePubSub.publish('editor.calculate.history.height');
+          }
+        );
+      }
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe(
+    'detach.scrolls',
+    snippet => {
+      let scrollElement = $('#snippet_' + snippet.id()).find('.dataTables_wrapper');
+      if (viewModel.editorMode()) {
+        scrollElement = $(window.MAIN_SCROLLABLE);
+      }
+      if (scrollElement.data('scrollFnDt')) {
+        scrollElement.off('scroll', scrollElement.data('scrollFnDt'));
+      }
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe(
+    'editor.calculate.history.height',
+    () => {
+      if (
+        viewModel.editorMode() &&
+        (viewModel.selectedNotebook().historyInitialHeight() === 0 ||
+          viewModel.selectedNotebook().forceHistoryInitialHeight())
+      ) {
+        let h = $('#queryHistory .history-table').height();
+        if (h === 0) {
+          h = viewModel.selectedNotebook().history().length * 32;
+        }
+        viewModel.selectedNotebook().historyInitialHeight(h + 80); // add pagination too
+        viewModel.selectedNotebook().forceHistoryInitialHeight(false);
+      }
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe('editor.create.new', newKeyHandler, HUE_PUB_SUB_EDITOR_ID);
+
+  if (viewModel.isOptimizerEnabled()) {
+    if (window.OPTIMIZER_AUTO_UPLOAD_QUERIES) {
+      huePubSub.subscribe(
+        'editor.upload.query',
+        query_id => {
+          viewModel
+            .selectedNotebook()
+            .snippets()[0]
+            .uploadQuery(query_id);
+        },
+        HUE_PUB_SUB_EDITOR_ID
+      );
+    }
+
+    if (window.OPTIMIZER_AUTO_UPLOAD_DDL) {
+      huePubSub.subscribe(
+        'editor.upload.table.stats',
+        options => {
+          viewModel
+            .selectedNotebook()
+            .snippets()[0]
+            .uploadTableStats(options);
+        },
+        HUE_PUB_SUB_EDITOR_ID
+      );
+    }
+
+    if (window.OPTIMIZER_QUERY_HISTORY_UPLOAD_LIMIT !== 0) {
+      huePubSub.subscribe(
+        'editor.upload.history',
+        () => {
+          viewModel
+            .selectedNotebook()
+            .snippets()[0]
+            .uploadQueryHistory(5);
+        },
+        HUE_PUB_SUB_EDITOR_ID
+      );
+    }
+  }
+
+  huePubSub.subscribe(
+    'get.selected.notebook',
+    () => {
+      huePubSub.publish('set.selected.notebook', viewModel.selectedNotebook());
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe(
+    'left.assist.show',
+    () => {
+      if (!viewModel.isLeftPanelVisible() && viewModel.assistAvailable()) {
+        viewModel.isLeftPanelVisible(true);
+      }
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe(
+    'assist.set.manual.visibility',
+    () => {
+      wasLeftPanelVisible = viewModel.isLeftPanelVisible();
+      wasRightPanelVisible = viewModel.isRightPanelVisible();
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe(
+    'editor.presentation.operate.toggle',
+    value => {
+      viewModel.isEditing(!viewModel.isEditing());
+      if (value) {
+        $('.jHueNotify').remove();
+        isAssistAvailable = viewModel.assistAvailable();
+        wasLeftPanelVisible = viewModel.isLeftPanelVisible();
+        wasRightPanelVisible = viewModel.isRightPanelVisible();
+
+        if (wasResultFullScreenMode) {
+          huePubSub.publish('both.assists.hide', true);
+        } else {
+          huePubSub.publish('right.assist.hide', true);
+        }
+
+        viewModel.assistWithoutStorage(true);
+        viewModel.assistAvailable(false);
+        viewModel.isLeftPanelVisible(true);
+        viewModel.isRightPanelVisible(false);
+        window.setTimeout(() => {
+          viewModel.assistWithoutStorage(false);
+        }, 0);
+        $('.navigator').hide();
+        $('.add-snippet').hide();
+
+        if (window.BANNER_TOP_HTML) {
+          $('.main-content').attr('style', 'top: 31px !important');
+        } else {
+          $('.main-content').css('top', '1px');
+        }
+        redrawFixedHeaders(200);
+        $(window).bind('keydown', 'esc', exitPlayerMode);
+      } else {
+        hideFixedHeaders();
+        huePubSub.publish('both.assists.show', true);
+        viewModel.assistWithoutStorage(true);
+        viewModel.isLeftPanelVisible(wasLeftPanelVisible);
+        viewModel.isRightPanelVisible(wasRightPanelVisible);
+        viewModel.assistAvailable(isAssistAvailable);
+        window.setTimeout(() => {
+          viewModel.assistWithoutStorage(false);
+        }, 0);
+        $('.navigator').show();
+        $('.add-snippet').show();
+        if (window.BANNER_TOP_HTML) {
+          $('.main-content').css('top', '112px');
+        } else {
+          $('.main-content').css('top', '74px');
+        }
+        redrawFixedHeaders(200);
+        $(window).unbind('keydown', exitPlayerMode);
+      }
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe(
+    'show.retry.modal',
+    data => {
+      $('#retryModal' + window.EDITOR_SUFFIX).modal('show');
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe(
+    'hide.retry.modal',
+    data => {
+      $('#retryModal' + window.EDITOR_SUFFIX).modal('hide');
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe(
+    'open.link',
+    link => {
+      $(window).unbind('keydown.editor');
+      if (link.indexOf('editor') >= 0) {
+        initKeydownBindings();
+      }
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe(
+    'editor.presentation.toggle',
+    () => {
+      viewModel.selectedNotebook().isPresentationMode(!viewModel.isPresentationMode());
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe('editor.save', saveKeyHandler, HUE_PUB_SUB_EDITOR_ID);
+
+  huePubSub.subscribe(
+    'editor.render.data',
+    options => {
+      const $snip = $('#snippet_' + options.snippet.id());
+      const _el = $snip.find('.resultTable');
+      if (options.data.length > 0) {
+        window.setTimeout(() => {
+          let _dt;
+          if (options.initial) {
+            options.snippet.result.meta.notifySubscribers();
+            $('#snippet_' + options.snippet.id())
+              .find('select')
+              .trigger('chosen:updated');
+            _dt = createDatatable(_el, options.snippet, viewModel);
+            resetResultsResizer(options.snippet);
+          } else {
+            _dt = _el.hueDataTable();
+          }
+          try {
+            _dt.fnAddData(options.data);
+          } catch (e) {}
+          const _dtElement = $snip.find('.dataTables_wrapper');
+          huePubSub.publish('editor.snippet.result.normal', options.snippet);
+          _dtElement.scrollTop(_dtElement.data('scrollPosition'));
+          redrawFixedHeaders();
+          resizeToggleResultSettings(options.snippet, options.initial);
+        }, 300);
+      } else {
+        huePubSub.publish('editor.snippet.result.normal', options.snippet);
+      }
+      $snip.find('select').trigger('chosen:updated');
+      $snip.find('.snippet-grid-settings').scrollLeft(0);
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe(
+    'editor.redraw.data',
+    options => {
+      hueUtils.waitForRendered(
+        '#snippet_' + options.snippet.id() + ' .resultTable',
+        el => {
+          return el.is(':visible');
+        },
+        () => {
+          const $el = $('#snippet_' + options.snippet.id()).find('.resultTable');
+          const dt = createDatatable($el, options.snippet, viewModel);
+          dt.fnAddData(options.snippet.result.data());
+        }
+      );
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe(
+    'editor.snippet.result.gray',
+    snippet => {
+      const $snippet = $('#snippet_' + snippet.id());
+      $snippet.find('.dataTables_wrapper .fixed-first-column').css({ opacity: '0' });
+      $snippet.find('.dataTables_wrapper .fixed-header-row').css({ opacity: '0' });
+      $snippet.find('.dataTables_wrapper .fixed-first-cell').css({ opacity: '0' });
+      $snippet.find('.dataTables_wrapper .resultTable').css({ opacity: '0.55' });
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe(
+    'editor.snippet.result.normal',
+    snippet => {
+      const $snippet = $('#snippet_' + snippet.id());
+      $snippet.find('.dataTables_wrapper .fixed-first-column').css({ opacity: '1' });
+      $snippet.find('.dataTables_wrapper .fixed-header-row').css({ opacity: '1' });
+      $snippet.find('.dataTables_wrapper .fixed-first-cell').css({ opacity: '1' });
+      $snippet.find('.dataTables_wrapper .resultTable').css({ opacity: '1' });
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe(
+    'render.jqcron',
+    () => {
+      if (typeof window.renderJqCron !== 'undefined') {
+        window.renderJqCron();
+      }
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe(
+    'submit.popup.return',
+    data => {
+      viewModel.selectedNotebook().viewSchedulerId(data.job_id);
+      $('.submit-modal-editor').modal('hide');
+      huePubSub.publish('show.jobs.panel', { id: data.job_id, interface: 'workflows' });
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe(
+    'jobbrowser.data',
+    jobs => {
+      const snippet = viewModel.selectedNotebook().snippets()[0];
+      if (!snippet || snippet.type() === 'impala') {
+        return;
+      }
+      if (jobs.length > 0) {
+        let progress = 0;
+        let parent;
+        jobs.forEach(job => {
+          const id = job.shortId || job.id;
+          const el = $('.jobs-overlay li:contains(' + id + ')');
+          if (!el.length) {
+            return;
+          }
+          const context = ko.contextFor(el[0]);
+          parent = context.$parent;
+          const _job = context.$data;
+          progress = parseInt(job.mapsPercentComplete);
+          if (isNaN(progress)) {
+            progress = parseInt(job.progress);
+          }
+          if (!isNaN(progress)) {
+            _job.percentJob(progress);
+          } else {
+            progress = 0;
+          }
+        });
+        if (parent && parent.jobs().length === 1) {
+          parent.progress(Math.max(progress, parent.progress()));
+        }
+      }
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe(
+    'editor.get.active.risks',
+    callback => {
+      const result = {
+        editor: undefined,
+        risks: {}
+      };
+      if (viewModel.selectedNotebook()) {
+        if (viewModel.selectedNotebook().snippets().length === 1) {
+          result.editor = viewModel
+            .selectedNotebook()
+            .snippets()[0]
+            .ace();
+          result.risks =
+            viewModel
+              .selectedNotebook()
+              .snippets()[0]
+              .complexity() || {};
+        } else {
+          viewModel
+            .selectedNotebook()
+            .snippets()
+            .every(snippet => {
+              if (snippet.inFocus()) {
+                result.editor = snippet.ace();
+                result.risks = snippet.complexity() || {};
+                return false;
+              }
+              return true;
+            });
+        }
+      }
+      callback(result);
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe(
+    'editor.grid.shown',
+    snippet => {
+      hueUtils.waitForRendered(
+        '#snippet_' + snippet.id() + ' .dataTables_wrapper',
+        el => {
+          return el.is(':visible');
+        },
+        () => {
+          resizeToggleResultSettings(snippet, true);
+          forceChartDraws();
+          $('#snippet_' + snippet.id())
+            .find('.snippet-grid-settings')
+            .scrollLeft(0);
+        }
+      );
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe(
+    'editor.chart.shown',
+    snippet => {
+      resizeToggleResultSettings(snippet, true);
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  huePubSub.subscribe(
+    'recalculate.name.description.width',
+    () => {
+      hueUtils.waitForRendered(
+        '.editorComponents .hue-title-bar .query-name',
+        el => {
+          return el.is(':visible');
+        },
+        () => {
+          let cumulativeWidth = 0;
+          $('.editorComponents .hue-title-bar ul li:not(.skip-width-calculation)').each(function() {
+            cumulativeWidth += $(this).outerWidth();
+          });
+          $('.notebook-name-desc').css(
+            'max-width',
+            ($('.editorComponents .hue-title-bar').width() -
+              cumulativeWidth -
+              $('.editorComponents .hue-title-bar .pull-right').width() -
+              120) /
+              2 +
+              'px'
+          );
+        }
+      );
+    },
+    HUE_PUB_SUB_EDITOR_ID
+  );
+
+  $(document).on('updateResultHeaders', e => {
+    hideFixedHeaders();
+    redrawFixedHeaders(200);
+  });
+
+  $(document).on('showAuthModal', (e, data) => {
+    viewModel.authSessionUsername(window.LOGGED_USERNAME);
+    viewModel.authSessionPassword('');
+    viewModel.authSessionType(data['type']);
+    viewModel.authSessionCallback(data['callback']);
+    $('#authModal' + window.EDITOR_SUFFIX).modal('show');
+  });
+
+  $(document).on('hideHistoryModal', e => {
+    $('#clearHistoryModal' + window.EDITOR_SUFFIX).modal('hide');
+  });
+
+  $(document).on('toggleResultSettings', (e, snippet) => {
+    window.setTimeout(() => {
+      const $snip = $('#snippet_' + snippet.id());
+      $snip.find('.chart').trigger('forceUpdate');
+      $snip.find('.snippet-grid-settings').scrollLeft(0);
+      if (snippet.isResultSettingsVisible()) {
+        $snip
+          .find('.table-results .grid-side')
+          .width(
+            100 -
+              $snip.find('.table-results .column-side').data('newWidth') -
+              hueUtils.bootstrapRatios.margin() +
+              '%'
+          );
+      } else {
+        $snip.find('.table-results .grid-side').width('100%');
+      }
+      redrawFixedHeaders();
+      $(window).trigger('resize');
+    }, 10);
+  });
+
+  $(document).on('editorSizeChanged', () => {
+    window.setTimeout(forceChartDraws, 50);
+  });
+
+  $(document).on('redrawResults', () => {
+    window.setTimeout(forceChartDraws, 50);
+  });
+
+  $(document).on('executeStarted', (e, options) => {
+    const $snip = $('#snippet_' + options.snippet.id());
+    const $el = $snip.find('.resultTable');
+    if (options.vm.editorMode()) {
+      $('#queryResults').css({
+        height: $el.height() + 'px'
+      });
+    }
+    $el.data('scrollToCol', null);
+    $el.data('scrollToRow', null);
+    $snip.find('.progress-snippet').animate(
+      {
+        height: '3px'
+      },
+      100
+    );
+    if ($el.hasClass('dt')) {
+      $el.removeClass('dt');
+      $('#eT' + options.snippet.id() + 'jHueTableExtenderClonedContainer').remove();
+      $('#eT' + options.snippet.id() + 'jHueTableExtenderClonedContainerColumn').remove();
+      $('#eT' + options.snippet.id() + 'jHueTableExtenderClonedContainerCell').remove();
+      if ($el.hueDataTable()) {
+        $el.hueDataTable().fnDestroy();
+      }
+      $el.find('thead tr').empty();
+      $el.data('lockedRows', {});
+    }
+  });
+
+  $(document).on('renderDataError', (e, options) => {
+    huePubSub.publish('editor.snippet.result.normal', options.snippet);
+  });
+
+  $(document).on('progress', (e, options) => {
+    if (options.data === 100) {
+      window.setTimeout(() => {
+        $('#snippet_' + options.snippet.id())
+          .find('.progress-snippet')
+          .animate(
+            {
+              height: '0'
+            },
+            100,
+            () => {
+              options.snippet.progress(0);
+              redrawFixedHeaders();
+            }
+          );
+      }, 2000);
+    }
+  });
+
+  $(document).on('forceChartDraw', (e, snippet) => {
+    window.setTimeout(() => {
+      snippet.chartX.notifySubscribers();
+      snippet.chartX.valueHasMutated();
+    }, 100);
+  });
+
+  let hideTimeout = -1;
+  $(document).on('hideAutocomplete', () => {
+    window.clearTimeout(hideTimeout);
+    hideTimeout = window.setTimeout(() => {
+      const $aceAutocomplete = $('.ace_editor.ace_autocomplete');
+      if ($aceAutocomplete.is(':visible')) {
+        $aceAutocomplete.hide();
+      }
+    }, 100);
+  });
+
+  let _resizeTimeout = -1;
+  $(window).on('resize', () => {
+    huePubSub.publish('recalculate.name.description.width');
+    window.clearTimeout(_resizeTimeout);
+    _resizeTimeout = window.setTimeout(() => {
+      forceChartDraws();
+    }, 200);
+  });
+});

+ 473 - 0
desktop/core/src/desktop/js/apps/notebook/chartTransformers.js

@@ -0,0 +1,473 @@
+// 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 HueColors from '../../utils/hueColors';
+import hueUtils from '../../utils/hueUtils';
+
+const isNotNullForCharts = val => val !== 'NULL' && val !== null;
+
+const pieChartTransformer = function(rawDatum) {
+  let _data = [];
+
+  if (rawDatum.snippet.chartX() != null && rawDatum.snippet.chartYSingle() != null) {
+    let _idxValue = -1;
+    let _idxLabel = -1;
+    rawDatum.snippet.result.meta().forEach((col, idx) => {
+      if (col.name === rawDatum.snippet.chartX()) {
+        _idxLabel = idx;
+      }
+      if (col.name === rawDatum.snippet.chartYSingle()) {
+        _idxValue = idx;
+      }
+    });
+    const colors = HueColors.cuiD3Scale();
+    $(rawDatum.counts()).each((cnt, item) => {
+      if (isNotNullForCharts(item[_idxValue])) {
+        let val = item[_idxValue] * 1;
+        if (isNaN(val)) {
+          val = 0;
+        }
+        _data.push({
+          label: hueUtils.html2text(item[_idxLabel]),
+          value: val,
+          color: colors[cnt % colors.length],
+          obj: item
+        });
+      }
+    });
+  }
+
+  if (rawDatum.sorting === 'asc') {
+    _data.sort((a, b) => a.value - b.value);
+  } else if (rawDatum.sorting === 'desc') {
+    _data.sort((a, b) => b.value - a.value);
+  }
+
+  if (rawDatum.snippet.chartLimit()) {
+    _data = _data.slice(0, rawDatum.snippet.chartLimit());
+  }
+
+  return _data;
+};
+
+const mapChartTransformer = function(rawDatum) {
+  let _data = [];
+  if (rawDatum.snippet.chartX() != null && rawDatum.snippet.chartYSingle() != null) {
+    let _idxRegion = -1;
+    let _idxValue = -1;
+    rawDatum.snippet.result.meta().forEach((col, idx) => {
+      if (col.name === rawDatum.snippet.chartX()) {
+        _idxRegion = idx;
+      }
+      if (col.name === rawDatum.snippet.chartYSingle()) {
+        _idxValue = idx;
+      }
+    });
+
+    $(rawDatum.counts()).each((cnt, item) => {
+      if (isNotNullForCharts(item[_idxValue]) && isNotNullForCharts(item[_idxRegion])) {
+        _data.push({
+          label: item[_idxRegion],
+          value: item[_idxValue],
+          obj: item
+        });
+      }
+    });
+  }
+
+  if (rawDatum.snippet.chartLimit()) {
+    _data = _data.slice(0, rawDatum.snippet.chartLimit());
+  }
+
+  return _data;
+};
+
+// The leaflet map can freeze the browser with numbers outside the map
+const MIN_LAT = -90;
+const MAX_LAT = 90;
+const MIN_LNG = -180;
+const MAX_LNG = 180;
+
+const leafletMapChartTransformer = function(rawDatum) {
+  let _data = [];
+  if (rawDatum.snippet.chartX() != null && rawDatum.snippet.chartYSingle() != null) {
+    let _idxLat = -1;
+    let _idxLng = -1;
+    let _idxLabel = -1;
+    let _idxHeat = -1;
+    rawDatum.snippet.result.meta().forEach((col, idx) => {
+      if (col.name === rawDatum.snippet.chartX()) {
+        _idxLat = idx;
+      }
+      if (col.name === rawDatum.snippet.chartYSingle()) {
+        _idxLng = idx;
+      }
+      if (col.name === rawDatum.snippet.chartMapLabel()) {
+        _idxLabel = idx;
+      }
+      if (col.name === rawDatum.snippet.chartMapHeat()) {
+        _idxHeat = idx;
+      }
+    });
+    if (rawDatum.snippet.chartMapLabel() != null) {
+      $(rawDatum.counts()).each((cnt, item) => {
+        if (isNotNullForCharts(item[_idxLat]) && isNotNullForCharts(item[_idxLng])) {
+          _data.push({
+            lat: Math.min(Math.max(MIN_LAT, item[_idxLat]), MAX_LAT),
+            lng: Math.min(Math.max(MIN_LNG, item[_idxLng]), MAX_LNG),
+            label: hueUtils.html2text(item[_idxLabel]),
+            isHeat: rawDatum.snippet.chartMapType() === 'heat',
+            intensity:
+              _idxHeat > -1 ? (item[_idxHeat] * 1 != NaN ? item[_idxHeat] * 1 : null) : null,
+            obj: item
+          });
+        }
+      });
+    } else {
+      $(rawDatum.counts()).each((cnt, item) => {
+        if (isNotNullForCharts(item[_idxLat]) && isNotNullForCharts(item[_idxLng])) {
+          _data.push({
+            lat: Math.min(Math.max(MIN_LAT, item[_idxLat]), MAX_LAT),
+            lng: Math.min(Math.max(MIN_LNG, item[_idxLng]), MAX_LNG),
+            isHeat: rawDatum.snippet.chartMapType() === 'heat',
+            intensity:
+              _idxHeat > -1 ? (item[_idxHeat] * 1 != NaN ? item[_idxHeat] * 1 : null) : null,
+            obj: item
+          });
+        }
+      });
+    }
+  }
+
+  if (rawDatum.snippet.chartLimit()) {
+    _data = _data.slice(0, rawDatum.snippet.chartLimit());
+  }
+
+  return _data;
+};
+
+const timelineChartTransformer = function(rawDatum) {
+  const _datum = [];
+  let _plottedSerie = 0;
+
+  rawDatum.snippet.result.meta().forEach(meta => {
+    if (rawDatum.snippet.chartYMulti().indexOf(meta.name) > -1) {
+      const col = meta.name;
+      let _idxValue = -1;
+      let _idxLabel = -1;
+      rawDatum.snippet.result.meta().forEach((icol, idx) => {
+        if (icol.name === rawDatum.snippet.chartX()) {
+          _idxLabel = idx;
+        }
+        if (icol.name === col) {
+          _idxValue = idx;
+        }
+      });
+
+      if (_idxValue > -1) {
+        let _data = [];
+        const colors = HueColors.cuiD3Scale();
+        $(rawDatum.counts()).each((cnt, item) => {
+          if (isNotNullForCharts(item[_idxLabel]) && isNotNullForCharts(item[_idxValue])) {
+            _data.push({
+              series: _plottedSerie,
+              x: new Date(moment(hueUtils.html2text(item[_idxLabel])).valueOf()),
+              y: item[_idxValue] * 1,
+              color: colors[_plottedSerie % colors.length],
+              obj: item
+            });
+          }
+        });
+        if (rawDatum.sorting === 'asc') {
+          _data.sort((a, b) => {
+            return a.y - b.y;
+          });
+        }
+        if (rawDatum.sorting === 'desc') {
+          _data.sort((a, b) => {
+            return b.y - a.y;
+          });
+        }
+        if (rawDatum.snippet.chartLimit()) {
+          _data = _data.slice(0, rawDatum.snippet.chartLimit());
+        }
+        _datum.push({
+          key: col,
+          values: _data
+        });
+        _plottedSerie++;
+      }
+    }
+  });
+
+  return _datum;
+};
+
+const multiSerieChartTransformer = function(rawDatum) {
+  let _datum = [];
+
+  if (rawDatum.snippet.chartX() != null && rawDatum.snippet.chartYMulti().length > 0) {
+    let _plottedSerie = 0;
+
+    if (typeof rawDatum.snippet.chartXPivot() !== 'undefined') {
+      let _idxValue = -1;
+      let _idxLabel = -1;
+      let _isXDate = false;
+
+      rawDatum.snippet.result.meta().forEach((icol, idx) => {
+        if (icol.name === rawDatum.snippet.chartX()) {
+          _isXDate = icol.type.toUpperCase().indexOf('DATE') > -1;
+          _idxLabel = idx;
+        }
+        if (icol.name === rawDatum.snippet.chartYSingle()) {
+          _idxValue = idx;
+        }
+      });
+
+      rawDatum.snippet.result.meta().forEach((meta, cnt) => {
+        if (rawDatum.snippet.chartXPivot() === meta.name) {
+          const _idxPivot = cnt;
+          const colors = HueColors.cuiD3Scale();
+          let pivotValues = $.map(rawDatum.counts(), p => {
+            return p[_idxPivot];
+          });
+          pivotValues = pivotValues.filter((item, pos) => {
+            return pivotValues.indexOf(item) === pos;
+          });
+          pivotValues.forEach((val, pivotCnt) => {
+            const _data = [];
+            $(rawDatum.counts()).each((cnt, item) => {
+              if (item[_idxPivot] === val) {
+                if (isNotNullForCharts(item[_idxValue]) && isNotNullForCharts(item[_idxLabel])) {
+                  _data.push({
+                    x: _isXDate ? moment(item[_idxLabel]) : hueUtils.html2text(item[_idxLabel]),
+                    y: item[_idxValue] * 1,
+                    color: colors[pivotCnt % colors.length],
+                    obj: item
+                  });
+                }
+              }
+            });
+            _datum.push({
+              key: hueUtils.html2text(val),
+              values: _data
+            });
+          });
+        }
+      });
+
+      // fills in missing values
+      let longest = 0;
+      const allXValues = [];
+      _datum.forEach(d => {
+        d.values.forEach(val => {
+          if (allXValues.indexOf(val.x) === -1) {
+            allXValues.push(val.x);
+          }
+        });
+      });
+
+      _datum.forEach(d => {
+        allXValues.forEach(val => {
+          if (
+            !d.values.some(item => {
+              return item.x === val;
+            })
+          ) {
+            const zeroObj = jQuery.extend({}, d.values[0]);
+            zeroObj.y = 0;
+            zeroObj.x = val;
+            d.values.push(zeroObj);
+          }
+        });
+        if (d.values.length > longest) {
+          longest = d.values.length;
+        }
+      });
+
+      // this is to avoid D3 js errors when the data the user is trying to display is bogus
+      if (allXValues.length < longest) {
+        _datum.forEach(d => {
+          for (let i = d.values.length; i < longest; i++) {
+            const zeroObj = jQuery.extend({}, d.values[0]);
+            zeroObj.y = 0;
+            zeroObj.x = '';
+            d.values.push(zeroObj);
+          }
+        });
+      }
+
+      if (rawDatum.snippet.chartLimit()) {
+        _datum = _datum.slice(0, rawDatum.snippet.chartLimit());
+      }
+
+      if (rawDatum.sorting === 'desc') {
+        _datum.forEach(d => {
+          d.values.sort((a, b) => {
+            if (a.x > b.x) {
+              return -1;
+            }
+            if (a.x < b.x) {
+              return 1;
+            }
+            return 0;
+          });
+        });
+      } else {
+        _datum.forEach(d => {
+          d.values.sort((a, b) => {
+            if (a.x > b.x) {
+              return 1;
+            }
+            if (a.x < b.x) {
+              return -1;
+            }
+            return 0;
+          });
+        });
+      }
+    } else {
+      rawDatum.snippet.result.meta().forEach(meta => {
+        if (rawDatum.snippet.chartYMulti().indexOf(meta.name) > -1) {
+          const col = meta.name;
+          let _idxValue = -1;
+          let _idxLabel = -1;
+          let _isXDate = false;
+          rawDatum.snippet.result.meta().forEach((icol, idx) => {
+            if (icol.name === rawDatum.snippet.chartX()) {
+              _isXDate = icol.type.toUpperCase().indexOf('DATE') > -1;
+              _idxLabel = idx;
+            }
+            if (icol.name === col) {
+              _idxValue = idx;
+            }
+          });
+
+          if (_idxValue > -1) {
+            let _data = [];
+            const colors = HueColors.cuiD3Scale();
+            $(rawDatum.counts()).each((cnt, item) => {
+              if (isNotNullForCharts(item[_idxValue]) && isNotNullForCharts(item[_idxLabel])) {
+                _data.push({
+                  series: _plottedSerie,
+                  x: _isXDate ? moment(item[_idxLabel]) : hueUtils.html2text(item[_idxLabel]),
+                  y: item[_idxValue] * 1,
+                  color: colors[cnt % colors.length],
+                  obj: item
+                });
+              }
+            });
+            if (rawDatum.sorting === 'asc') {
+              _data.sort((a, b) => {
+                return a.y - b.y;
+              });
+            }
+            if (rawDatum.sorting === 'desc') {
+              _data.sort((a, b) => {
+                return b.y - a.y;
+              });
+            }
+            if (rawDatum.snippet.chartLimit()) {
+              _data = _data.slice(0, rawDatum.snippet.chartLimit());
+            }
+            _datum.push({
+              key: col,
+              values: _data
+            });
+            _plottedSerie++;
+          }
+        }
+      });
+    }
+  }
+  return _datum;
+};
+
+const scatterChartTransformer = function(rawDatum) {
+  const datum = {};
+
+  if (rawDatum.snippet.chartX() != null && rawDatum.snippet.chartYSingle() != null) {
+    let idxX = -1;
+    let idxY = -1;
+    let idxSize = -1;
+    let idxGroup = -1;
+    rawDatum.snippet.result.meta().forEach((icol, idx) => {
+      if (icol.name === rawDatum.snippet.chartX()) {
+        idxX = idx;
+      }
+      if (icol.name === rawDatum.snippet.chartYSingle()) {
+        idxY = idx;
+      }
+      if (icol.name === rawDatum.snippet.chartScatterSize()) {
+        idxSize = idx;
+      }
+      if (icol.name === rawDatum.snippet.chartScatterGroup()) {
+        idxGroup = idx;
+      }
+    });
+
+    if (idxX > -1 && idxY > -1) {
+      const createAndAddToArray = function(key, item) {
+        if (!datum[key]) {
+          datum[key] = [];
+        }
+        if (isNotNullForCharts(item[idxX]) && isNotNullForCharts(item[idxY])) {
+          datum[key].push({
+            x: item[idxX],
+            y: item[idxY],
+            shape: 'circle',
+            size: idxSize > -1 ? item[idxSize] : 100,
+            obj: item
+          });
+        }
+      };
+
+      if (idxGroup > -1) {
+        $(rawDatum.counts()).each((cnt, item) => {
+          createAndAddToArray(item[idxGroup], item);
+        });
+      } else {
+        $(rawDatum.counts()).each((cnt, item) => {
+          createAndAddToArray('distro', item);
+        });
+      }
+    }
+  }
+
+  const returndDatum = [];
+
+  Object.keys(datum).forEach(key => {
+    returndDatum.push({
+      key: key,
+      values: rawDatum.snippet.chartLimit()
+        ? datum[key].slice(0, rawDatum.snippet.chartLimit())
+        : datum[key]
+    });
+  });
+
+  return returndDatum;
+};
+
+export default {
+  pie: pieChartTransformer,
+  map: mapChartTransformer,
+  leafletMap: leafletMapChartTransformer,
+  timeline: timelineChartTransformer,
+  multiSerie: multiSerieChartTransformer,
+  scatter: scatterChartTransformer
+};

+ 663 - 0
desktop/core/src/desktop/js/apps/notebook/editorViewModel.js

@@ -0,0 +1,663 @@
+// 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 komapping from 'knockout.mapping';
+
+import apiHelper from 'api/apiHelper';
+import ChartTransformers from 'apps/notebook/chartTransformers';
+import huePubSub from 'utils/huePubSub';
+import hueUtils from 'utils/hueUtils';
+
+import Notebook from 'apps/notebook/notebook';
+import Snippet from 'apps/notebook/snippet';
+
+class EditorViewModel {
+  constructor(editor_id, notebooks, options, CoordinatorEditorViewModel, RunningCoordinatorModel) {
+    const self = this;
+
+    self.URLS = {
+      editor: '/hue/editor',
+      notebook: '/hue/notebook',
+      report: '/hue/dashboard/new_search?engine=report'
+    };
+
+    self.huePubSubId = options.huePubSubId || 'editor';
+    self.user = options.user;
+    self.userId = options.userId;
+    self.suffix = options.suffix;
+    self.isMobile = ko.observable(options.mobile);
+    self.isNotificationManager = ko.observable(options.is_notification_manager || false);
+    self.editorType = ko.observable(options.editor_type);
+    self.editorType.subscribe(newVal => {
+      self.editorMode(newVal != 'notebook');
+      hueUtils.changeURLParameter('type', newVal);
+      if (self.editorMode()) {
+        self.selectedNotebook().fetchHistory(); // Js error if notebook did not have snippets
+      }
+    });
+    self.preEditorTogglingSnippet = ko.observable();
+    self.toggleEditorMode = function() {
+      const _notebook = self.selectedNotebook();
+      const _newSnippets = [];
+
+      if (self.editorType() != 'notebook') {
+        self.editorType('notebook');
+        self.preEditorTogglingSnippet(_notebook.snippets()[0]);
+        const _variables = _notebook.snippets()[0].variables();
+        const _statementKeys = [];
+        // Split statements
+        _notebook.type('notebook');
+        _notebook
+          .snippets()[0]
+          .statementsList()
+          .forEach(sql_statement => {
+            let _snippet;
+            if (sql_statement.hashCode() in _notebook.presentationSnippets()) {
+              _snippet = _notebook.presentationSnippets()[sql_statement.hashCode()]; // Persist result
+              _snippet.variables(_variables);
+            } else {
+              const _title = [];
+              const _statement = [];
+              sql_statement
+                .trim()
+                .split('\n')
+                .forEach(line => {
+                  if (line.trim().startsWith('--') && _statement.length === 0) {
+                    _title.push(line.substr(2));
+                  } else {
+                    _statement.push(line);
+                  }
+                });
+              _snippet = new Snippet(self, _notebook, {
+                type: _notebook.initialType,
+                statement_raw: _statement.join('\n'),
+                result: {},
+                name: _title.join('\n'),
+                variables: komapping.toJS(_variables)
+              });
+              _snippet.variables = _notebook.snippets()[0].variables;
+              _snippet.init();
+              _notebook.presentationSnippets()[sql_statement.hashCode()] = _snippet;
+            }
+            _statementKeys.push(sql_statement.hashCode());
+            _newSnippets.push(_snippet);
+          });
+        $.each(_notebook.presentationSnippets(), key => {
+          // Dead statements
+          if (!key in _statementKeys) {
+            delete _notebook.presentationSnippets()[key];
+          }
+        });
+      } else {
+        self.editorType(_notebook.initialType);
+        // Revert to one statement
+        _newSnippets.push(self.preEditorTogglingSnippet());
+        _notebook.type('query-' + _notebook.initialType);
+      }
+      _notebook.snippets(_newSnippets);
+      _newSnippets.forEach(snippet => {
+        huePubSub.publish('editor.redraw.data', { snippet: snippet });
+      });
+    };
+    self.togglePresentationMode = function() {
+      if (self.selectedNotebook().initialType !== 'notebook') {
+        self.toggleEditorMode();
+      }
+    };
+    self.editorTypeTitle = ko.pureComputed(() => {
+      const foundInterpreter = $.grep(options.languages, interpreter => {
+        return interpreter.type === self.editorType();
+      });
+      return foundInterpreter.length > 0 ? foundInterpreter[0].name : self.editorType();
+    });
+    self.autocompleteTimeout = options.autocompleteTimeout;
+    self.selectedNotebook = ko.observable();
+
+    self.combinedContent = ko.observable();
+    self.isPresentationModeEnabled = ko.pureComputed(() => {
+      return (
+        self.selectedNotebook() &&
+        self.selectedNotebook().snippets().length === 1 &&
+        self
+          .selectedNotebook()
+          .snippets()[0]
+          .isSqlDialect()
+      );
+    });
+    self.isResultFullScreenMode = ko.observable(false);
+    self.isPresentationMode = ko.computed(() => {
+      return self.selectedNotebook() && self.selectedNotebook().isPresentationMode();
+    });
+    self.isHidingCode = ko.computed(() => {
+      return self.selectedNotebook() && self.selectedNotebook().isHidingCode();
+    });
+    self.successUrl = ko.observable(options.success_url); // Deprecated
+    self.isOptimizerEnabled = ko.observable(options.is_optimizer_enabled);
+    self.isNavigatorEnabled = ko.observable(options.is_navigator_enabled);
+
+    self.CoordinatorEditorViewModel = CoordinatorEditorViewModel;
+    self.RunningCoordinatorModel = RunningCoordinatorModel;
+
+    self.canSave = ko.computed(() => {
+      // Saved query or history but history coming from a saved query
+      return (
+        self.selectedNotebook() &&
+        self.selectedNotebook().canWrite() &&
+        (self.selectedNotebook().isSaved() ||
+          (self.selectedNotebook().isHistory() && self.selectedNotebook().parentSavedQueryUuid()))
+      );
+    });
+
+    self.ChartTransformers = ChartTransformers;
+
+    // TODO: Drop the SQL source types from the notebook. They're now set in AssistDbPanel.
+    self.sqlSourceTypes = [];
+    self.availableLanguages = [];
+
+    if (options.languages && options.snippetViewSettings) {
+      $.each(options.languages, (idx, language) => {
+        self.availableLanguages.push({
+          type: language.type,
+          name: language.name,
+          interface: language.interface
+        });
+        const viewSettings = options.snippetViewSettings[language.type];
+        if (viewSettings && viewSettings.sqlDialect) {
+          self.sqlSourceTypes.push({
+            type: language.type,
+            name: language.name
+          });
+        }
+      });
+    }
+
+    const sqlSourceTypes = $.grep(self.sqlSourceTypes, language => {
+      return language.type == self.editorType();
+    });
+    if (sqlSourceTypes.length > 0) {
+      self.activeSqlSourceType = sqlSourceTypes[0].type;
+    } else {
+      self.activeSqlSourceType = null;
+    }
+
+    self.displayCombinedContent = function() {
+      if (!self.selectedNotebook()) {
+        self.combinedContent('');
+      } else {
+        let statements = '';
+        $.each(self.selectedNotebook().snippets(), (index, snippet) => {
+          if (snippet.statement()) {
+            if (statements) {
+              statements += '\n\n';
+            }
+            statements += snippet.statement();
+          }
+        });
+        self.combinedContent(statements);
+      }
+      $('#combinedContentModal' + self.suffix).modal('show');
+    };
+
+    self.isEditing = ko.observable(false);
+    self.isEditing.subscribe(() => {
+      $(document).trigger('editingToggled');
+    });
+    self.toggleEditing = function() {
+      self.isEditing(!self.isEditing());
+    };
+
+    self.authSessionUsername = ko.observable(); // UI popup
+    self.authSessionPassword = ko.observable();
+    self.authSessionType = ko.observable();
+    self.authSessionCallback = ko.observable();
+
+    self.removeSnippetConfirmation = ko.observable();
+
+    self.removeSnippet = function(notebook, snippet) {
+      let hasContent = snippet.statement_raw().length > 0;
+      if (!hasContent) {
+        $.each(snippet.properties(), (key, value) => {
+          hasContent = hasContent || (ko.isObservable(value) && value().length > 0);
+        });
+      }
+      if (hasContent) {
+        self.removeSnippetConfirmation({ notebook: notebook, snippet: snippet });
+        $('#removeSnippetModal' + self.suffix).modal('show');
+      } else {
+        notebook.snippets.remove(snippet);
+        window.setTimeout(() => {
+          $(document).trigger('editorSizeChanged');
+        }, 100);
+      }
+    };
+
+    self.assistAvailable = ko.observable(options.assistAvailable);
+
+    self.assistWithoutStorage = ko.observable(false);
+
+    self.isLeftPanelVisible = ko.observable(
+      apiHelper.getFromTotalStorage('assist', 'assist_panel_visible', true)
+    );
+    self.isLeftPanelVisible.subscribe(val => {
+      if (!self.assistWithoutStorage()) {
+        apiHelper.setInTotalStorage('assist', 'assist_panel_visible', val);
+      }
+    });
+
+    self.isRightPanelAvailable = ko.observable(options.assistAvailable && HAS_OPTIMIZER);
+    self.isRightPanelVisible = ko.observable(
+      apiHelper.getFromTotalStorage('assist', 'right_assist_panel_visible', true)
+    );
+    self.isRightPanelVisible.subscribe(val => {
+      if (!self.assistWithoutStorage()) {
+        apiHelper.setInTotalStorage('assist', 'right_assist_panel_visible', val);
+      }
+    });
+
+    const withActiveSnippet = function(callback) {
+      const notebook = self.selectedNotebook();
+      let foundSnippet;
+      if (notebook) {
+        if (notebook.snippets().length === 1) {
+          foundSnippet = notebook.snippets()[0];
+        } else {
+          notebook.snippets().every(snippet => {
+            if (snippet.inFocus()) {
+              foundSnippet = snippet;
+              return false;
+            }
+            return true;
+          });
+        }
+      }
+      if (foundSnippet) {
+        callback(foundSnippet);
+      }
+    };
+
+    huePubSub.subscribe('assist.highlight.risk.suggestions', () => {
+      if (self.isRightPanelAvailable() && !self.isRightPanelVisible()) {
+        self.isRightPanelVisible(true);
+      }
+    });
+
+    self.isContextPanelVisible = ko.observable(false);
+    self.isContextPanelVisible.subscribe(newValue => {
+      huePubSub.publish('context.panel.visible', newValue);
+    });
+
+    huePubSub.subscribe('context.panel.visible.editor', self.isContextPanelVisible);
+
+    huePubSub.subscribe(
+      'get.active.snippet.type',
+      () => {
+        withActiveSnippet(activeSnippet => {
+          huePubSub.publish('set.active.snippet.type', activeSnippet.type());
+        });
+      },
+      self.huePubSubId
+    );
+
+    huePubSub.subscribe(
+      'save.snippet.to.file',
+      () => {
+        withActiveSnippet(activeSnippet => {
+          const data = {
+            path: activeSnippet.statementPath(),
+            contents: activeSnippet.statement()
+          };
+          const options = {
+            successCallback: function(result) {
+              if (result && result.exists) {
+                $(document).trigger('info', result.path + ' saved successfully.');
+              } else {
+                self._ajaxError(result);
+              }
+            }
+          };
+          apiHelper.saveSnippetToFile(data, options);
+        });
+      },
+      self.huePubSubId
+    );
+
+    huePubSub.subscribe(
+      'sql.context.pin',
+      contextData => {
+        withActiveSnippet(activeSnippet => {
+          contextData.tabId = 'context' + activeSnippet.pinnedContextTabs().length;
+          activeSnippet.pinnedContextTabs.push(contextData);
+          activeSnippet.currentQueryTab(contextData.tabId);
+        });
+      },
+      self.huePubSubId
+    );
+
+    huePubSub.subscribe(
+      'assist.database.set',
+      databaseDef => {
+        withActiveSnippet(activeSnippet => {
+          activeSnippet.handleAssistSelection(databaseDef);
+        });
+      },
+      self.huePubSubId
+    );
+
+    huePubSub.subscribe(
+      'assist.database.selected',
+      databaseDef => {
+        withActiveSnippet(activeSnippet => {
+          activeSnippet.handleAssistSelection(databaseDef);
+        });
+      },
+      self.huePubSubId
+    );
+
+    self.availableSnippets = komapping.fromJS(options.languages);
+
+    self.editorMode = ko.observable(options.mode == 'editor');
+
+    self.getSnippetViewSettings = function(snippetType) {
+      if (options.snippetViewSettings[snippetType]) {
+        return options.snippetViewSettings[snippetType];
+      }
+      return options.snippetViewSettings.default;
+    };
+
+    self.availableSessionProperties = ko.computed(() => {
+      // Only Spark
+      return ko.utils.arrayFilter(options.session_properties, item => {
+        return item.name != ''; // Could filter out the ones already selected + yarn only or not
+      });
+    });
+    self.getSessionProperties = function(name) {
+      let _prop = null;
+      $.each(options.session_properties, (index, prop) => {
+        if (prop.name == name) {
+          _prop = prop;
+          return;
+        }
+      });
+      return _prop;
+    };
+
+    self.getSnippetName = function(snippetType) {
+      const availableSnippets = self.availableSnippets();
+      for (let i = 0; i < availableSnippets.length; i++) {
+        if (availableSnippets[i].type() === snippetType) {
+          return availableSnippets[i].name();
+        }
+      }
+      return '';
+    };
+
+    self.changeURL = function(url) {
+      if (!self.isNotificationManager()) {
+        hueUtils.changeURL(url);
+      }
+    };
+
+    self.init = function() {
+      if (editor_id) {
+        self.openNotebook(editor_id);
+      } else if (window.location.getParameter('editor') !== '') {
+        self.openNotebook(window.location.getParameter('editor'));
+      } else if (notebooks.length > 0) {
+        self.loadNotebook(notebooks[0]); // Old way of loading json for /browse
+      } else if (window.location.getParameter('type') !== '') {
+        self.newNotebook(window.location.getParameter('type'));
+      } else {
+        self.newNotebook();
+      }
+    };
+
+    self.loadNotebook = function(notebookRaw, queryTab) {
+      let currentQueries;
+      if (self.selectedNotebook() != null) {
+        currentQueries = self.selectedNotebook().unload();
+      }
+
+      const notebook = new Notebook(self, notebookRaw);
+
+      if (notebook.snippets().length > 0) {
+        huePubSub.publish('detach.scrolls', notebook.snippets()[0]);
+        notebook.selectedSnippet(notebook.snippets()[notebook.snippets().length - 1].type());
+        if (currentQueries != null) {
+          notebook.snippets()[0].queries(currentQueries);
+        }
+        notebook.snippets().forEach(snippet => {
+          snippet.aceAutoExpand = false;
+          snippet.statement_raw.valueHasMutated();
+          if (
+            snippet.result.handle().statements_count > 1 &&
+            snippet.result.handle().start != null &&
+            snippet.result.handle().end != null
+          ) {
+            const aceLineOffset = snippet.result.handle().aceLineOffset || 0;
+            snippet.result.statement_range({
+              start: {
+                row: snippet.result.handle().start.row + aceLineOffset,
+                column: snippet.result.handle().start.column
+              },
+              end: {
+                row: snippet.result.handle().end.row + aceLineOffset,
+                column: snippet.result.handle().end.column
+              }
+            });
+            snippet.result.statement_range.valueHasMutated();
+          }
+
+          snippet.previousChartOptions = self._getPreviousChartOptions(snippet);
+        });
+
+        if (notebook.snippets()[0].result.data().length > 0) {
+          $(document).trigger('redrawResults');
+        } else if (queryTab) {
+          notebook.snippets()[0].currentQueryTab(queryTab);
+        }
+
+        if (notebook.isSaved()) {
+          notebook.snippets()[0].currentQueryTab('savedQueries');
+          if (notebook.snippets()[0].queries().length === 0) {
+            notebook.snippets()[0].fetchQueries(); // Subscribe not updating yet
+          }
+        }
+      }
+
+      self.selectedNotebook(notebook);
+      huePubSub.publish('check.job.browser');
+      huePubSub.publish('recalculate.name.description.width');
+    };
+
+    self._getPreviousChartOptions = function(snippet) {
+      return {
+        chartLimit:
+          typeof snippet.chartLimit() !== 'undefined'
+            ? snippet.chartLimit()
+            : snippet.previousChartOptions.chartLimit,
+        chartX:
+          typeof snippet.chartX() !== 'undefined'
+            ? snippet.chartX()
+            : snippet.previousChartOptions.chartX,
+        chartXPivot:
+          typeof snippet.chartXPivot() !== 'undefined'
+            ? snippet.chartXPivot()
+            : snippet.previousChartOptions.chartXPivot,
+        chartYSingle:
+          typeof snippet.chartYSingle() !== 'undefined'
+            ? snippet.chartYSingle()
+            : snippet.previousChartOptions.chartYSingle,
+        chartMapType:
+          typeof snippet.chartMapType() !== 'undefined'
+            ? snippet.chartMapType()
+            : snippet.previousChartOptions.chartMapType,
+        chartMapLabel:
+          typeof snippet.chartMapLabel() !== 'undefined'
+            ? snippet.chartMapLabel()
+            : snippet.previousChartOptions.chartMapLabel,
+        chartMapHeat:
+          typeof snippet.chartMapHeat() !== 'undefined'
+            ? snippet.chartMapHeat()
+            : snippet.previousChartOptions.chartMapHeat,
+        chartYMulti:
+          typeof snippet.chartYMulti() !== 'undefined'
+            ? snippet.chartYMulti()
+            : snippet.previousChartOptions.chartYMulti,
+        chartScope:
+          typeof snippet.chartScope() !== 'undefined'
+            ? snippet.chartScope()
+            : snippet.previousChartOptions.chartScope,
+        chartTimelineType:
+          typeof snippet.chartTimelineType() !== 'undefined'
+            ? snippet.chartTimelineType()
+            : snippet.previousChartOptions.chartTimelineType,
+        chartSorting:
+          typeof snippet.chartSorting() !== 'undefined'
+            ? snippet.chartSorting()
+            : snippet.previousChartOptions.chartSorting,
+        chartScatterGroup:
+          typeof snippet.chartScatterGroup() !== 'undefined'
+            ? snippet.chartScatterGroup()
+            : snippet.previousChartOptions.chartScatterGroup,
+        chartScatterSize:
+          typeof snippet.chartScatterSize() !== 'undefined'
+            ? snippet.chartScatterSize()
+            : snippet.previousChartOptions.chartScatterSize
+      };
+    };
+
+    self.openNotebook = function(uuid, queryTab, skipUrlChange, callback) {
+      const deferredOpen = new $.Deferred();
+      $.get(
+        '/desktop/api2/doc/',
+        {
+          uuid: uuid,
+          data: true,
+          dependencies: true
+        },
+        data => {
+          if (data.status == 0) {
+            data.data.dependents = data.dependents;
+            data.data.can_write = data.user_perms.can_write;
+            const notebook = data.data;
+            self.loadNotebook(notebook, queryTab);
+            if (typeof skipUrlChange === 'undefined' && !self.isNotificationManager()) {
+              if (self.editorMode()) {
+                self.editorType(data.document.type.substring('query-'.length));
+                huePubSub.publish('active.snippet.type.changed', self.editorType());
+                self.changeURL(
+                  self.URLS.editor + '?editor=' + data.document.id + '&type=' + self.editorType()
+                );
+              } else {
+                self.changeURL(self.URLS.notebook + '?notebook=' + data.document.id);
+              }
+            }
+            if (typeof callback !== 'undefined') {
+              callback();
+            }
+            deferredOpen.resolve();
+          } else {
+            $(document).trigger('error', data.message);
+            deferredOpen.reject();
+            self.newNotebook();
+          }
+        }
+      );
+      return deferredOpen.promise();
+    };
+
+    self.newNotebook = function(editorType, callback, queryTab) {
+      huePubSub.publish('active.snippet.type.changed', editorType);
+      $.post(
+        '/notebook/api/create_notebook',
+        {
+          type: editorType || options.editor_type,
+          directory_uuid: window.location.getParameter('directory_uuid')
+        },
+        data => {
+          self.loadNotebook(data.notebook);
+          if (self.editorMode() && !self.isNotificationManager()) {
+            const snippet = self.selectedNotebook().newSnippet(self.editorType());
+            if (
+              queryTab &&
+              ['queryHistory', 'savedQueries', 'queryBuilderTab'].indexOf(queryTab) > -1
+            ) {
+              snippet.currentQueryTab(queryTab);
+            }
+            huePubSub.publish('detach.scrolls', self.selectedNotebook().snippets()[0]);
+            if (window.location.getParameter('type') === '') {
+              hueUtils.changeURLParameter('type', self.editorType());
+            }
+            huePubSub.publish('active.snippet.type.changed', editorType);
+          }
+
+          if (typeof callback !== 'undefined' && callback !== null) {
+            callback();
+          }
+        }
+      );
+    };
+
+    self.saveNotebook = function() {
+      self.selectedNotebook().save();
+    };
+
+    self.saveAsNotebook = function() {
+      self.selectedNotebook().id(null);
+      self.selectedNotebook().uuid(hueUtils.UUID());
+      self.selectedNotebook().parentSavedQueryUuid(null);
+      self.selectedNotebook().save(() => {
+        huePubSub.publish('assist.document.refresh');
+      });
+    };
+
+    self.showContextPopover = function(field, event) {
+      const $source = $(
+        event.target && event.target.nodeName !== 'A' ? event.target.parentElement : event.target
+      );
+      const offset = $source.offset();
+      huePubSub.publish('context.popover.show', {
+        data: {
+          type: 'catalogEntry',
+          catalogEntry: field.catalogEntry
+        },
+        onSampleClick: field.value,
+        showInAssistEnabled: true,
+        sourceType: self.editorType(),
+        orientation: 'bottom',
+        defaultDatabase: 'default',
+        pinEnabled: false,
+        source: {
+          element: event.target,
+          left: offset.left,
+          top: offset.top - 3,
+          right: offset.left + $source.width() + 1,
+          bottom: offset.top + $source.height() - 3
+        }
+      });
+    };
+  }
+
+  prepareShareModal() {
+    const selectedNotebookUuid = this.selectedNotebook() && this.selectedNotebook().uuid();
+    window.shareViewModel.setDocUuid(selectedNotebookUuid);
+    window.openShareModal();
+  }
+}
+
+export default EditorViewModel;

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

@@ -14,12 +14,939 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import Clipboard from 'clipboard';
-import 'jquery-mousewheel';
+import $ from 'jquery';
+import ko from 'knockout';
+import komapping from 'knockout.mapping';
 
-import 'ext/bootstrap-datepicker.min';
-import 'ext/jquery.hotkeys';
-import 'jquery/plugins/jquery.hdfstree';
-import 'ext/markdown.min';
+import apiHelper from 'api/apiHelper';
+import hueAnalytics from 'utils/hueAnalytics';
+import huePubSub from 'utils/huePubSub';
+import hueUtils from 'utils/hueUtils';
 
-window.Clipboard = Clipboard;
+import Session from 'apps/notebook/session';
+import Snippet from 'apps/notebook/snippet';
+
+const NOTEBOOK_MAPPING = {
+  ignore: [
+    'ace',
+    'aceMode',
+    'autocompleter',
+    'availableDatabases',
+    'availableSnippets',
+    'avoidClosing',
+    'canWrite',
+    'cleanedDateTimeMeta',
+    'cleanedMeta',
+    'cleanedNumericMeta',
+    'cleanedStringMeta',
+    'dependents',
+    'errorLoadingQueries',
+    'hasProperties',
+    'history',
+    'images',
+    'inFocus',
+    'queries',
+    'saveResultsModalVisible',
+    'selectedStatement',
+    'snippetImage',
+    'user',
+    'positionStatement',
+    'lastExecutedStatement',
+    'downloadResultViewModel'
+  ]
+};
+
+class Notebook {
+  constructor(vm, notebook) {
+    const self = this;
+
+    self.id = ko.observable(
+      typeof notebook.id != 'undefined' && notebook.id != null ? notebook.id : null
+    );
+    self.uuid = ko.observable(
+      typeof notebook.uuid != 'undefined' && notebook.uuid != null ? notebook.uuid : hueUtils.UUID()
+    );
+    self.name = ko.observable(
+      typeof notebook.name != 'undefined' && notebook.name != null ? notebook.name : 'My Notebook'
+    );
+    self.description = ko.observable(
+      typeof notebook.description != 'undefined' && notebook.description != null
+        ? notebook.description
+        : ''
+    );
+    self.type = ko.observable(
+      typeof notebook.type != 'undefined' && notebook.type != null ? notebook.type : 'notebook'
+    );
+    self.initialType = self.type().replace('query-', '');
+    self.coordinatorUuid = ko.observable(
+      typeof notebook.coordinatorUuid != 'undefined' && notebook.coordinatorUuid != null
+        ? notebook.coordinatorUuid
+        : null
+    );
+    self.isHistory = ko.observable(
+      typeof notebook.is_history != 'undefined' && notebook.is_history != null
+        ? notebook.is_history
+        : false
+    );
+    self.isManaged = ko.observable(
+      typeof notebook.isManaged != 'undefined' && notebook.isManaged != null
+        ? notebook.isManaged
+        : false
+    );
+    self.parentSavedQueryUuid = ko.observable(
+      typeof notebook.parentSavedQueryUuid != 'undefined' && notebook.parentSavedQueryUuid != null
+        ? notebook.parentSavedQueryUuid
+        : null
+    ); // History parent
+    self.isSaved = ko.observable(
+      typeof notebook.isSaved != 'undefined' && notebook.isSaved != null ? notebook.isSaved : false
+    );
+    self.canWrite = ko.observable(
+      typeof notebook.can_write != 'undefined' && notebook.can_write != null
+        ? notebook.can_write
+        : true
+    );
+    self.onSuccessUrl = ko.observable(
+      typeof notebook.onSuccessUrl != 'undefined' && notebook.onSuccessUrl != null
+        ? notebook.onSuccessUrl
+        : null
+    );
+    self.pubSubUrl = ko.observable(
+      typeof notebook.pubSubUrl != 'undefined' && notebook.pubSubUrl != null
+        ? notebook.pubSubUrl
+        : null
+    );
+    self.isPresentationModeDefault = ko.observable(
+      typeof notebook.isPresentationModeDefault != 'undefined' &&
+        notebook.isPresentationModeDefault != null
+        ? notebook.isPresentationModeDefault
+        : false
+    );
+    self.isPresentationMode = ko.observable(false);
+    self.isPresentationModeInitialized = ko.observable(false);
+    self.isPresentationMode.subscribe(newValue => {
+      if (!newValue) {
+        self.cancelExecutingAll();
+      }
+      huePubSub.publish('editor.presentation.operate.toggle', newValue); // Problem with headers / row numbers redraw on full screen results
+      vm.togglePresentationMode();
+      if (newValue) {
+        hueAnalytics.convert('editor', 'presentation');
+      }
+    });
+    self.presentationSnippets = ko.observable({});
+    self.isHidingCode = ko.observable(
+      typeof notebook.isHidingCode != 'undefined' && notebook.isHidingCode != null
+        ? notebook.isHidingCode
+        : false
+    );
+
+    self.snippets = ko.observableArray();
+    self.selectedSnippet = ko.observable(vm.editorType()); // Aka selectedSnippetType
+    self.creatingSessionLocks = ko.observableArray();
+    self.sessions = komapping.fromJS(
+      typeof notebook.sessions != 'undefined' && notebook.sessions != null ? notebook.sessions : [],
+      {
+        create: function(value) {
+          return new Session(vm, value.data);
+        }
+      }
+    );
+    self.directoryUuid = ko.observable(
+      typeof notebook.directoryUuid != 'undefined' && notebook.directoryUuid != null
+        ? notebook.directoryUuid
+        : null
+    );
+    self.dependents = komapping.fromJS(
+      typeof notebook.dependents != 'undefined' && notebook.dependents != null
+        ? notebook.dependents
+        : []
+    );
+    self.dependentsCoordinator = ko.computed(() => {
+      return $.grep(self.dependents(), doc => {
+        return doc.type() == 'oozie-coordinator2' && doc.is_managed() == true;
+      });
+    });
+    if (self.dependentsCoordinator().length > 0 && !self.coordinatorUuid()) {
+      self.coordinatorUuid(self.dependentsCoordinator()[0].uuid());
+    }
+    self.history = ko.observableArray(
+      vm.selectedNotebook() &&
+        vm.selectedNotebook().history().length > 0 &&
+        vm.selectedNotebook().history()[0].type == self.type()
+        ? vm.selectedNotebook().history()
+        : []
+    );
+    self.history.subscribe(val => {
+      if (
+        self.id() == null &&
+        val.length == 0 &&
+        self.historyFilter() === '' &&
+        !vm.isNotificationManager()
+      ) {
+        self
+          .snippets()[0]
+          .currentQueryTab(
+            typeof IS_EMBEDDED !== 'undefined' && IS_EMBEDDED ? 'queryHistory' : 'savedQueries'
+          );
+      }
+    });
+    self.historyFilter = ko.observable('');
+    self.historyFilterVisible = ko.observable(false);
+    self.historyFilter.extend({ rateLimit: { method: 'notifyWhenChangesStop', timeout: 900 } });
+    self.historyFilter.subscribe(val => {
+      if (self.historyCurrentPage() != 1) {
+        self.historyCurrentPage(1);
+      } else {
+        self.fetchHistory();
+      }
+    });
+    self.loadingHistory = ko.observable(self.history().length == 0);
+    self.historyInitialHeight = ko.observable(0).extend({ throttle: 1000 });
+    self.forceHistoryInitialHeight = ko.observable(false);
+    self.historyCurrentPage = ko.observable(
+      vm.selectedNotebook() ? vm.selectedNotebook().historyCurrentPage() : 1
+    );
+    self.historyCurrentPage.subscribe(val => {
+      self.fetchHistory();
+    });
+    self.historyTotalPages = ko.observable(
+      vm.selectedNotebook() ? vm.selectedNotebook().historyTotalPages() : 1
+    );
+
+    self.schedulerViewModel = null;
+    self.schedulerViewModelIsLoaded = ko.observable(false);
+    self.schedulerViewerViewModel = ko.observable();
+    self.isBatchable = ko.computed(() => {
+      return (
+        self.snippets().length > 0 &&
+        $.grep(self.snippets(), snippet => {
+          return snippet.isBatchable();
+        }).length == self.snippets().length
+      );
+    });
+
+    self.isExecutingAll = ko.observable(!!notebook.isExecutingAll);
+    self.cancelExecutingAll = function() {
+      const index = self.executingAllIndex();
+      if (self.isExecutingAll() && self.snippets()[index]) {
+        self.snippets()[index].cancel();
+      }
+    };
+    self.executingAllIndex = ko.observable(notebook.executingAllIndex || 0);
+
+    self.retryModalConfirm = null;
+    self.retryModalCancel = null;
+
+    self.avoidClosing = false;
+
+    self.canSave = vm.canSave;
+
+    self.getSession = function(session_type) {
+      let _s = null;
+      $.each(self.sessions(), (index, s) => {
+        if (s.type() == session_type) {
+          _s = s;
+          return false;
+        }
+      });
+      return _s;
+    };
+
+    self.getSnippets = function(type) {
+      return $.grep(self.snippets(), snippet => {
+        return snippet.type() == type;
+      });
+    };
+
+    self.unloaded = ko.observable(false);
+    self.unload = function() {
+      self.unloaded(true);
+      let currentQueries = null;
+      self.snippets().forEach(snippet => {
+        if (snippet.checkStatusTimeout != null) {
+          clearTimeout(snippet.checkStatusTimeout);
+          snippet.checkStatusTimeout = null;
+        }
+        if (currentQueries == null) {
+          currentQueries = snippet.queries();
+        }
+      });
+      return currentQueries;
+    };
+
+    self.restartSession = function(session, callback) {
+      if (session.restarting()) {
+        return;
+      }
+      session.restarting(true);
+      const snippets = self.getSnippets(session.type());
+
+      $.each(snippets, (index, snippet) => {
+        snippet.status('loading');
+      });
+
+      self.closeSession(session, true, () => {
+        self.createSession(
+          session,
+          () => {
+            $.each(snippets, (index, snippet) => {
+              snippet.status('ready');
+            });
+            session.restarting(false);
+            if (callback) {
+              callback();
+            }
+          },
+          () => {
+            session.restarting(false);
+          }
+        );
+      });
+    };
+
+    self.addSession = function(session) {
+      const toRemove = [];
+      $.each(self.sessions(), (index, s) => {
+        if (s.type() == session.type()) {
+          toRemove.push(s);
+        }
+      });
+
+      $.each(toRemove, (index, s) => {
+        self.sessions.remove(s);
+      });
+
+      self.sessions.push(session);
+    };
+
+    self.addSnippet = function(snippet, skipSession) {
+      const _snippet = new Snippet(vm, self, snippet);
+      self.snippets.push(_snippet);
+
+      if (self.getSession(_snippet.type()) == null && typeof skipSession == 'undefined') {
+        window.setTimeout(() => {
+          _snippet.status('loading');
+          self.createSession(new Session(vm, { type: _snippet.type() }));
+        }, 200);
+      }
+
+      _snippet.init();
+      return _snippet;
+    };
+
+    self.createSession = function(session, callback, failCallback) {
+      if (self.creatingSessionLocks().indexOf(session.type()) != -1) {
+        // Create one type of session max
+        return;
+      } else {
+        self.creatingSessionLocks.push(session.type());
+      }
+
+      let compute = null;
+      $.each(self.getSnippets(session.type()), (index, snippet) => {
+        snippet.status('loading');
+        if (index == 0) {
+          compute = snippet.compute();
+        }
+      });
+
+      const fail = function(message) {
+        $.each(self.getSnippets(session.type()), (index, snippet) => {
+          snippet.status('failed');
+        });
+        $(document).trigger('error', message);
+        if (failCallback) {
+          failCallback();
+        }
+      };
+
+      $.post(
+        '/notebook/api/create_session',
+        {
+          notebook: komapping.toJSON(self.getContext()),
+          session: komapping.toJSON(session), // e.g. {'type': 'pyspark', 'properties': [{'name': driverCores', 'value', '2'}]}
+          cluster: komapping.toJSON(compute ? compute : '')
+        },
+        data => {
+          if (data.status == 0) {
+            komapping.fromJS(data.session, {}, session);
+            if (self.getSession(session.type()) == null) {
+              self.addSession(session);
+            }
+            $.each(self.getSnippets(session.type()), (index, snippet) => {
+              snippet.status('ready');
+            });
+            if (callback) {
+              setTimeout(callback, 500);
+            }
+          } else if (data.status == 401) {
+            $(document).trigger('showAuthModal', { type: session.type() });
+          } else {
+            fail(data.message);
+          }
+        }
+      )
+        .fail(xhr => {
+          if (xhr.status !== 502) {
+            fail(xhr.responseText);
+          }
+        })
+        .always(() => {
+          self.creatingSessionLocks.remove(session.type());
+        });
+    };
+
+    self.authSession = function() {
+      self.createSession(
+        new Session(vm, {
+          type: vm.authSessionType(),
+          properties: [
+            { name: 'user', value: vm.authSessionUsername() },
+            { name: 'password', value: vm.authSessionPassword() }
+          ]
+        }),
+        vm.authSessionCallback() // On new session we don't automatically execute the snippet after the aut. On session expiration we do or we refresh assist DB when login-in.
+      );
+    };
+
+    self.newSnippet = function(type) {
+      if (type) {
+        self.selectedSnippet(type);
+      }
+      const snippet = self.addSnippet({
+        type: self.selectedSnippet(),
+        result: {}
+      });
+
+      window.setTimeout(() => {
+        const lastSnippet = snippet;
+        if (lastSnippet.ace() != null) {
+          lastSnippet.ace().focus();
+        }
+      }, 100);
+
+      hueAnalytics.log('notebook', 'add_snippet/' + (type ? type : self.selectedSnippet()));
+      return snippet;
+    };
+
+    self.newSnippetAbove = function(id) {
+      self.newSnippet();
+      let idx = 0;
+      self.snippets().forEach((snippet, cnt) => {
+        if (snippet.id() == id) {
+          idx = cnt;
+        }
+      });
+      self.snippets(self.snippets().move(self.snippets().length - 1, idx));
+    };
+
+    self.getContext = function() {
+      return {
+        id: self.id,
+        uuid: self.uuid,
+        parentSavedQueryUuid: self.parentSavedQueryUuid,
+        isSaved: self.isSaved,
+        sessions: self.sessions,
+        type: self.type,
+        name: self.name
+      };
+    };
+
+    self.save = function(callback) {
+      hueAnalytics.log('notebook', 'save');
+
+      // Remove the result data from the snippets
+      // Also do it for presentation mode
+      const cp = komapping.toJS(self, NOTEBOOK_MAPPING);
+      $.each(
+        cp.snippets.concat(
+          Object.keys(cp.presentationSnippets).map(key => {
+            return cp.presentationSnippets[key];
+          })
+        ),
+        (index, snippet) => {
+          snippet.result.data.length = 0; // snippet.result.clear() does not work for some reason
+          snippet.result.meta.length = 0;
+          snippet.result.logs = '';
+          snippet.result.fetchedOnce = false;
+          snippet.progress = 0; // Remove progress
+          snippet.jobs.length = 0;
+        }
+      );
+      if (cp.schedulerViewModel) {
+        cp.schedulerViewModel.availableTimezones = [];
+      }
+      const editorMode =
+        vm.editorMode() || (self.isPresentationMode() && vm.editorType() != 'notebook'); // Editor should not convert to Notebook in presentation mode
+
+      $.post(
+        '/notebook/api/notebook/save',
+        {
+          notebook: komapping.toJSON(cp, NOTEBOOK_MAPPING),
+          editorMode: editorMode
+        },
+        data => {
+          if (data.status == 0) {
+            self.id(data.id);
+            self.isSaved(true);
+            const wasHistory = self.isHistory();
+            self.isHistory(false);
+            $(document).trigger('info', data.message);
+            if (editorMode) {
+              if (!data.save_as) {
+                const existingQuery = self
+                  .snippets()[0]
+                  .queries()
+                  .filter(item => {
+                    return item.uuid() === data.uuid;
+                  });
+                if (existingQuery.length > 0) {
+                  existingQuery[0].name(data.name);
+                  existingQuery[0].description(data.description);
+                  existingQuery[0].last_modified(data.last_modified);
+                }
+              } else if (self.snippets()[0].queries().length > 0) {
+                // Saved queries tab already loaded
+                self.snippets()[0].queries.unshift(komapping.fromJS(data));
+              }
+
+              if (self.coordinatorUuid() && self.schedulerViewModel) {
+                self.saveScheduler();
+                self.schedulerViewModel.coordinator.refreshParameters();
+              }
+              if (wasHistory || data.save_as) {
+                self.loadScheduler();
+              }
+
+              if (
+                self.snippets()[0].downloadResultViewModel &&
+                self
+                  .snippets()[0]
+                  .downloadResultViewModel()
+                  .saveTarget() === 'dashboard'
+              ) {
+                huePubSub.publish(
+                  'open.link',
+                  vm.URLS.report +
+                    '&uuid=' +
+                    data.uuid +
+                    '&statement=' +
+                    self.snippets()[0].result.handle().statement_id
+                );
+              } else {
+                vm.changeURL(vm.URLS.editor + '?editor=' + data.id);
+              }
+            } else {
+              vm.changeURL(vm.URLS.notebook + '?notebook=' + data.id);
+            }
+            if (typeof callback == 'function') {
+              callback();
+            }
+          } else {
+            $(document).trigger('error', data.message);
+          }
+        }
+      ).fail((xhr, textStatus, errorThrown) => {
+        if (xhr.status !== 502) {
+          $(document).trigger('error', xhr.responseText);
+        }
+      });
+    };
+
+    self.close = function() {
+      hueAnalytics.log('notebook', 'close');
+      $.post('/notebook/api/notebook/close', {
+        notebook: komapping.toJSON(self, NOTEBOOK_MAPPING),
+        editorMode: vm.editorMode()
+      });
+    };
+
+    self.clearResults = function() {
+      $.each(self.snippets(), (index, snippet) => {
+        snippet.result.clear();
+        snippet.status('ready');
+      });
+    };
+
+    self.executeAll = function() {
+      if (self.isExecutingAll() || self.snippets().length === 0) {
+        return;
+      }
+
+      self.isExecutingAll(true);
+      self.executingAllIndex(0);
+
+      self.snippets()[self.executingAllIndex()].execute();
+    };
+
+    self.saveDefaultUserProperties = function(session) {
+      apiHelper.saveConfiguration({
+        app: session.type(),
+        properties: session.properties,
+        userId: vm.userId
+      });
+    };
+
+    self.closeAndRemoveSession = function(session) {
+      self.closeSession(session, false, () => {
+        self.sessions.remove(session);
+      });
+    };
+
+    self.closeSession = function(session, silent, callback) {
+      $.post(
+        '/notebook/api/close_session',
+        {
+          session: komapping.toJSON(session)
+        },
+        data => {
+          if (!silent && data && data.status != 0 && data.status != -2 && data.message) {
+            $(document).trigger('error', data.message);
+          }
+
+          if (callback) {
+            callback();
+          }
+        }
+      ).fail(xhr => {
+        if (!silent && xhr.status !== 502) {
+          $(document).trigger('error', xhr.responseText);
+        }
+      });
+    };
+
+    self.fetchHistory = function(callback) {
+      const QUERIES_PER_PAGE = 50;
+      self.loadingHistory(true);
+
+      $.get(
+        '/notebook/api/get_history',
+        {
+          doc_type: self.selectedSnippet(),
+          limit: QUERIES_PER_PAGE,
+          page: self.historyCurrentPage(),
+          doc_text: self.historyFilter(),
+          is_notification_manager: vm.isNotificationManager()
+        },
+        data => {
+          const parsedHistory = [];
+          if (data && data.history) {
+            data.history.forEach(nbk => {
+              parsedHistory.push(
+                self._makeHistoryRecord(
+                  nbk.absoluteUrl,
+                  nbk.data.statement,
+                  nbk.data.lastExecuted,
+                  nbk.data.status,
+                  nbk.name,
+                  nbk.uuid
+                )
+              );
+            });
+          }
+          self.history(parsedHistory);
+          self.historyTotalPages(Math.ceil(data.count / QUERIES_PER_PAGE));
+        }
+      ).always(() => {
+        self.loadingHistory(false);
+        if (callback) {
+          callback();
+        }
+      });
+    };
+
+    self.prevHistoryPage = function() {
+      if (self.historyCurrentPage() !== 1) {
+        self.historyCurrentPage(self.historyCurrentPage() - 1);
+      }
+    };
+
+    self.nextHistoryPage = function() {
+      if (self.historyCurrentPage() < self.historyTotalPages()) {
+        self.historyCurrentPage(self.historyCurrentPage() + 1);
+      }
+    };
+
+    self.updateHistoryFailed = false;
+    self.updateHistory = function(statuses, interval) {
+      let items = $.grep(self.history(), item => {
+        return statuses.indexOf(item.status()) != -1;
+      }).slice(0, 25);
+
+      function updateHistoryCall(item) {
+        $.post('/notebook/api/check_status', {
+          notebook: komapping.toJSON({ id: item.uuid() })
+        })
+          .done(data => {
+            const status =
+              data.status == -3
+                ? 'expired'
+                : data.status == 0
+                ? data.query_status.status
+                : 'failed';
+            if (status && item.status() != status) {
+              item.status(status);
+            }
+          })
+          .fail(xhr => {
+            items = [];
+            self.updateHistoryFailed = true;
+            console.warn('Lost connectivity to the Hue history refresh backend.');
+          })
+          .always(() => {
+            if (items.length > 0) {
+              window.setTimeout(() => {
+                updateHistoryCall(items.pop());
+              }, 1000);
+            } else if (!self.updateHistoryFailed) {
+              window.setTimeout(() => {
+                self.updateHistory(statuses, interval);
+              }, interval);
+            }
+          });
+      }
+
+      if (items.length > 0) {
+        updateHistoryCall(items.pop());
+      } else if (!self.updateHistoryFailed) {
+        window.setTimeout(() => {
+          self.updateHistory(statuses, interval);
+        }, interval);
+      }
+    };
+
+    self._makeHistoryRecord = function(url, statement, lastExecuted, status, name, uuid) {
+      return komapping.fromJS({
+        url: url,
+        query: statement.substring(0, 1000) + (statement.length > 1000 ? '...' : ''),
+        lastExecuted: lastExecuted,
+        status: status,
+        name: name,
+        uuid: uuid
+      });
+    };
+
+    self.clearHistory = function(type) {
+      hueAnalytics.log('notebook', 'clearHistory');
+      $.post(
+        '/notebook/api/clear_history',
+        {
+          notebook: komapping.toJSON(self.getContext()),
+          doc_type: self.selectedSnippet(),
+          is_notification_manager: vm.isNotificationManager()
+        },
+        data => {
+          self.history.removeAll();
+          if (self.isHistory()) {
+            self.id(null);
+            self.uuid(hueUtils.UUID());
+            vm.changeURL(vm.URLS.editor + '?type=' + vm.editorType());
+          }
+        }
+      ).fail(xhr => {
+        if (xhr.status !== 502) {
+          $(document).trigger('error', xhr.responseText);
+        }
+      });
+      $(document).trigger('hideHistoryModal');
+    };
+
+    self.loadScheduler = function() {
+      if (typeof vm.CoordinatorEditorViewModel !== 'undefined' && self.isBatchable()) {
+        let _action;
+        if (self.coordinatorUuid()) {
+          _action = 'edit';
+        } else {
+          _action = 'new';
+        }
+        hueAnalytics.log('notebook', 'schedule/' + _action);
+
+        const getCoordinator = function() {
+          $.get(
+            '/oozie/editor/coordinator/' + _action + '/',
+            {
+              format: 'json',
+              document: self.uuid(),
+              coordinator: self.coordinatorUuid()
+            },
+            data => {
+              if ($('#schedulerEditor').length > 0) {
+                huePubSub.publish('hue4.process.headers', {
+                  response: data.layout,
+                  callback: function(r) {
+                    $('#schedulerEditor').html(r);
+
+                    self.schedulerViewModel = new vm.CoordinatorEditorViewModel(
+                      data.coordinator,
+                      data.credentials,
+                      data.workflows,
+                      data.can_edit
+                    );
+
+                    ko.cleanNode($('#schedulerEditor')[0]);
+                    ko.applyBindings(self.schedulerViewModel, $('#schedulerEditor')[0]);
+                    $(document).off('showSubmitPopup');
+                    $(document).on('showSubmitPopup', (event, data) => {
+                      $('.submit-modal-editor').html(data);
+                      $('.submit-modal-editor').modal('show');
+                      $('.submit-modal-editor').on('hidden', () => {
+                        huePubSub.publish('hide.datepicker');
+                      });
+                      const _sel = $('.submit-form .control-group[rel!="popover"]:visible');
+                      if (_sel.length > 0) {
+                        $('.submit-modal-editor .modal-body').height(
+                          $('.submit-modal-editor .modal-body').height() + 60
+                        );
+                      }
+                    });
+
+                    huePubSub.publish('render.jqcron');
+
+                    self.schedulerViewModel.coordinator.properties.cron_advanced.valueHasMutated(); // Update jsCron enabled status
+                    self.schedulerViewModel.coordinator.tracker().markCurrentStateAsClean();
+                    self.schedulerViewModel.isEditing(true);
+
+                    self.schedulerViewModelIsLoaded(true);
+
+                    if (_action == 'new') {
+                      self.schedulerViewModel.coordinator.properties.document(self.uuid()); // Expected for triggering the display
+                    }
+                  }
+                });
+              }
+            }
+          ).fail(xhr => {
+            if (xhr.status !== 502) {
+              $(document).trigger('error', xhr.responseText);
+            }
+          });
+        };
+
+        getCoordinator();
+      }
+    };
+
+    self.saveScheduler = function() {
+      if (
+        self.isBatchable() &&
+        (!self.coordinatorUuid() || self.schedulerViewModel.coordinator.isDirty())
+      ) {
+        self.schedulerViewModel.coordinator.isManaged(true);
+        self.schedulerViewModel.coordinator.properties.document(self.uuid());
+        self.schedulerViewModel.save(data => {
+          if (!self.coordinatorUuid()) {
+            self.coordinatorUuid(data.uuid);
+            self.save();
+          }
+        });
+      }
+    };
+
+    self.showSubmitPopup = function() {
+      $.get(
+        '/oozie/editor/coordinator/submit/' + self.coordinatorUuid(),
+        {
+          format: 'json'
+        },
+        data => {
+          $(document).trigger('showSubmitPopup', data);
+        }
+      ).fail((xhr, textStatus, errorThrown) => {
+        if (xhr.status !== 502) {
+          $(document).trigger('error', xhr.responseText);
+        }
+      });
+    };
+
+    self.viewSchedulerId = ko.observable(
+      typeof notebook.viewSchedulerId != 'undefined' && notebook.viewSchedulerId != null
+        ? notebook.viewSchedulerId
+        : ''
+    );
+    self.viewSchedulerId.subscribe(newVal => {
+      self.save();
+    });
+    self.isSchedulerJobRunning = ko.observable();
+    self.loadingScheduler = ko.observable(false);
+
+    // Init
+    if (notebook.snippets) {
+      $.each(notebook.snippets, (index, snippet) => {
+        self.addSnippet(snippet);
+      });
+      if (
+        typeof notebook.presentationSnippets != 'undefined' &&
+        notebook.presentationSnippets != null
+      ) {
+        // Load
+        $.each(notebook.presentationSnippets, (key, snippet) => {
+          snippet.status = 'ready'; // Protect from storm of check_statuses
+          const _snippet = new Snippet(vm, self, snippet);
+          _snippet.init();
+          _snippet.previousChartOptions = vm._getPreviousChartOptions(_snippet);
+          self.presentationSnippets()[key] = _snippet;
+        });
+      }
+      if (vm.editorMode() && self.history().length == 0) {
+        self.fetchHistory(() => {
+          self.updateHistory(['starting', 'running'], 30000);
+          self.updateHistory(['available'], 60000 * 5);
+        });
+      }
+    }
+
+    huePubSub.subscribeOnce(
+      'assist.db.panel.ready',
+      () => {
+        if (self.type().indexOf('query') === 0) {
+          const whenDatabaseAvailable = function(snippet) {
+            huePubSub.publish('assist.set.database', {
+              source: snippet.type(),
+              namespace: snippet.namespace(),
+              name: snippet.database()
+            });
+          };
+
+          const whenNamespaceAvailable = function(snippet) {
+            if (snippet.database()) {
+              whenDatabaseAvailable(snippet);
+            } else {
+              const databaseSub = snippet.database.subscribe(() => {
+                databaseSub.dispose();
+                whenDatabaseAvailable(snippet);
+              });
+            }
+          };
+
+          const whenSnippetAvailable = function(snippet) {
+            if (snippet.namespace()) {
+              whenNamespaceAvailable(snippet);
+            } else {
+              const namespaceSub = snippet.namespace.subscribe(() => {
+                namespaceSub.dispose();
+                whenNamespaceAvailable(snippet);
+              });
+            }
+          };
+
+          if (self.snippets().length === 1) {
+            whenSnippetAvailable(self.snippets()[0]);
+          } else {
+            const snippetsSub = self.snippets.subscribe(snippets => {
+              if (snippets.length === 1) {
+                whenSnippetAvailable(snippets[0]);
+              }
+              snippetsSub.dispose();
+            });
+          }
+        }
+      },
+      vm.huePubSubId
+    );
+
+    huePubSub.publish('assist.is.db.panel.ready');
+  }
+}
+
+export default Notebook;

+ 0 - 4457
desktop/core/src/desktop/js/apps/notebook/notebook.ko.js

@@ -1,4457 +0,0 @@
-// Licensed to Cloudera, Inc. under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  Cloudera, Inc. licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import $ from 'jquery';
-import ko from 'knockout';
-import komapping from 'knockout.mapping';
-
-import AceAutocompleteWrapper from 'apps/notebook/aceAutocompleteWrapper';
-import apiHelper from 'api/apiHelper';
-import dataCatalog from 'catalog/dataCatalog';
-import hueAnalytics from 'utils/hueAnalytics';
-import huePubSub from 'utils/huePubSub';
-import hueUtils from 'utils/hueUtils';
-
-const NOTEBOOK_MAPPING = {
-  ignore: [
-    'ace',
-    'aceMode',
-    'autocompleter',
-    'availableDatabases',
-    'availableSnippets',
-    'avoidClosing',
-    'canWrite',
-    'cleanedDateTimeMeta',
-    'cleanedMeta',
-    'cleanedNumericMeta',
-    'cleanedStringMeta',
-    'dependents',
-    'errorLoadingQueries',
-    'hasProperties',
-    'history',
-    'images',
-    'inFocus',
-    'queries',
-    'saveResultsModalVisible',
-    'selectedStatement',
-    'snippetImage',
-    'user',
-    'positionStatement',
-    'lastExecutedStatement',
-    'downloadResultViewModel'
-  ]
-};
-
-const COMPATIBILITY_SOURCE_PLATFORMS = {
-  teradata: { name: 'Teradata', value: 'teradata' },
-  oracle: { name: 'Oracle', value: 'oracle' },
-  netezza: { name: 'Netezza', value: 'netezza' },
-  impala: { name: 'Impala', value: 'impala' },
-  hive: { name: 'Hive', value: 'hive' },
-  db2: { name: 'DB2', value: 'db2' },
-  greenplum: { name: 'Greenplum', value: 'greenplum' },
-  mysql: { name: 'MySQL', value: 'mysql' },
-  postgresql: { name: 'PostgreSQL', value: 'postgresql' },
-  informix: { name: 'Informix', value: 'informix' },
-  sqlserver: { name: 'SQL Server', value: 'sqlserver' },
-  sybase: { name: 'Sybase', value: 'sybase' },
-  access: { name: 'Access', value: 'access' },
-  firebird: { name: 'Firebird', value: 'firebird' },
-  ansisql: { name: 'ANSISQL', value: 'ansisql' },
-  generic: { name: 'Generic', value: 'generic' }
-};
-
-const COMPATIBILITY_TARGET_PLATFORMS = {
-  impala: { name: 'Impala', value: 'impala' },
-  hive: { name: 'Hive', value: 'hive' }
-};
-
-const Result = function(snippet, result) {
-  const self = this;
-
-  $.extend(
-    snippet,
-    snippet.chartType == 'lines' && {
-      // Retire line chart
-      chartType: 'bars',
-      chartTimelineType: 'line'
-    }
-  );
-  self.id = ko.observable(
-    typeof result.id != 'undefined' && result.id != null ? result.id : hueUtils.UUID()
-  );
-  self.type = ko.observable(
-    typeof result.type != 'undefined' && result.type != null ? result.type : 'table'
-  );
-  self.hasResultset = ko
-    .observable(
-      typeof result.hasResultset != 'undefined' && result.hasResultset != null
-        ? result.hasResultset
-        : true
-    )
-    .extend('throttle', 100);
-  self.handle = ko.observable(
-    typeof result.handle != 'undefined' && result.handle != null ? result.handle : {}
-  );
-  self.meta = ko.observableArray(
-    typeof result.meta != 'undefined' && result.meta != null ? result.meta : []
-  );
-
-  const adaptMeta = function() {
-    let i = 0;
-    self.meta().forEach(item => {
-      if (typeof item.checked === 'undefined') {
-        item.checked = ko.observable(true);
-        item.checked.subscribe(() => {
-          self.filteredMetaChecked(
-            self.filteredMeta().some(item => {
-              return item.checked();
-            })
-          );
-        });
-      }
-      item.type = item.type.replace(/_type/i, '').toLowerCase();
-      if (typeof item.originalIndex === 'undefined') {
-        item.originalIndex = i;
-      }
-      i++;
-    });
-  };
-
-  adaptMeta();
-  self.meta.subscribe(adaptMeta);
-
-  self.rows = ko.observable(
-    typeof result.rows != 'undefined' && result.rows != null ? result.rows : null
-  );
-  self.hasMore = ko.observable(
-    typeof result.hasMore != 'undefined' && result.hasMore != null ? result.hasMore : false
-  );
-  self.statement_id = ko.observable(
-    typeof result.statement_id != 'undefined' && result.statement_id != null
-      ? result.statement_id
-      : 0
-  );
-  self.statement_range = ko.observable(
-    typeof result.statement_range != 'undefined' && result.statement_range != null
-      ? result.statement_range
-      : {
-          start: {
-            row: 0,
-            column: 0
-          },
-          end: {
-            row: 0,
-            column: 0
-          }
-        }
-  );
-  // We don't keep track of any previous selection so prevent entering into batch execution mode after load by setting
-  // statements_count to 1. For the case when a selection is not starting at row 0.
-  self.statements_count = ko.observable(1);
-  self.previous_statement_hash = ko.observable(
-    typeof result.previous_statement_hash != 'undefined' && result.previous_statement_hash != null
-      ? result.previous_statement_hash
-      : null
-  );
-  self.cleanedMeta = ko.computed(() => {
-    return ko.utils.arrayFilter(self.meta(), item => {
-      return item.name != '';
-    });
-  });
-  self.metaFilter = ko.observable();
-
-  self.isMetaFilterVisible = ko.observable(false);
-  self.filteredMetaChecked = ko.observable(true);
-  self.filteredMeta = ko.pureComputed(() => {
-    if (!self.metaFilter() || self.metaFilter().query === '') {
-      return self.meta();
-    }
-
-    return self.meta().filter(item => {
-      const facets = self.metaFilter().facets;
-      const isFacetMatch = !facets || Object.keys(facets).length === 0 || !facets['type']; // So far only type facet is used for SQL
-      const isTextMatch = !self.metaFilter().text || self.metaFilter().text.length === 0;
-      let match = true;
-
-      if (!isFacetMatch) {
-        match = !!facets['type'][item.type];
-      }
-
-      if (match && !isTextMatch) {
-        match = self.metaFilter().text.every(text => {
-          return item.name.toLowerCase().indexOf(text.toLowerCase()) !== -1;
-        });
-      }
-      return match;
-    });
-  });
-
-  self.autocompleteFromEntries = function(nonPartial, partial) {
-    const result = [];
-    const partialLower = partial.toLowerCase();
-    self.meta().forEach(column => {
-      if (column.name.toLowerCase().indexOf(partialLower) === 0) {
-        result.push(nonPartial + partial + column.name.substring(partial.length));
-      } else if (column.name.toLowerCase().indexOf('.' + partialLower) !== -1) {
-        result.push(
-          nonPartial +
-            partial +
-            column.name.substring(
-              partial.length + column.name.toLowerCase().indexOf('.' + partialLower) + 1
-            )
-        );
-      }
-    });
-
-    return result;
-  };
-  self.clickFilteredMetaCheck = function() {
-    self.filteredMeta().forEach(item => {
-      item.checked(self.filteredMetaChecked());
-    });
-  };
-
-  self.fetchedOnce = ko.observable(
-    typeof result.fetchedOnce != 'undefined' && result.fetchedOnce != null
-      ? result.fetchedOnce
-      : false
-  );
-  self.startTime = ko.observable(
-    typeof result.startTime != 'undefined' && result.startTime != null
-      ? new Date(result.startTime)
-      : new Date()
-  );
-  self.endTime = ko.observable(
-    typeof result.endTime != 'undefined' && result.endTime != null
-      ? new Date(result.endTime)
-      : new Date()
-  );
-  self.executionTime = ko.computed(() => {
-    return self.endTime().getTime() - self.startTime().getTime();
-  });
-
-  function isNumericColumn(type) {
-    return (
-      $.inArray(type, [
-        'tinyint',
-        'smallint',
-        'int',
-        'bigint',
-        'float',
-        'double',
-        'decimal',
-        'real'
-      ]) > -1
-    );
-  }
-
-  function isDateTimeColumn(type) {
-    return $.inArray(type, ['timestamp', 'date', 'datetime']) > -1;
-  }
-
-  function isComplexColumn(type) {
-    return $.inArray(type, ['array', 'map', 'struct']) > -1;
-  }
-
-  function isStringColumn(type) {
-    return !isNumericColumn(type) && !isDateTimeColumn(type) && !isComplexColumn(type);
-  }
-
-  self.cleanedNumericMeta = ko.computed(() => {
-    return ko.utils.arrayFilter(self.meta(), item => {
-      return item.name != '' && isNumericColumn(item.type);
-    });
-  });
-
-  self.cleanedStringMeta = ko.computed(() => {
-    return ko.utils.arrayFilter(self.meta(), item => {
-      return item.name != '' && isStringColumn(item.type);
-    });
-  });
-
-  self.cleanedDateTimeMeta = ko.computed(() => {
-    return ko.utils.arrayFilter(self.meta(), item => {
-      return item.name != '' && isDateTimeColumn(item.type);
-    });
-  });
-
-  self.data = ko.observableArray(
-    typeof result.data != 'undefined' && result.data != null ? result.data : []
-  );
-  self.data.extend({ rateLimit: 50 });
-  self.explanation = ko.observable(
-    typeof result.explanation != 'undefined' && result.explanation != null ? result.explanation : ''
-  );
-  self.images = ko.observableArray(
-    typeof result.images != 'undefined' && result.images != null ? result.images : []
-  );
-  self.images.extend({ rateLimit: 50 });
-  self.logs = ko.observable('');
-  self.logLines = 0;
-  self.hasSomeResults = ko.computed(() => {
-    return self.hasResultset() && self.data().length > 0; // status() == 'available'
-  });
-
-  self.getContext = function() {
-    return {
-      id: self.id,
-      type: self.type,
-      handle: self.handle
-    };
-  };
-
-  self.clear = function() {
-    self.fetchedOnce(false);
-    self.hasMore(false);
-    self.statement_range({
-      start: {
-        row: 0,
-        column: 0
-      },
-      end: {
-        row: 0,
-        column: 0
-      }
-    });
-    self.meta.removeAll();
-    self.data.removeAll();
-    self.images.removeAll();
-    self.logs('');
-    self.handle({
-      // Keep multiquery indexing
-      has_more_statements: self.handle()['has_more_statements'],
-      statement_id: self.handle()['statement_id'],
-      statements_count: self.handle()['statements_count'],
-      previous_statement_hash: self.handle()['previous_statement_hash']
-    });
-    self.startTime(new Date());
-    self.endTime(new Date());
-    self.explanation('');
-    self.logLines = 0;
-    self.rows(null);
-  };
-};
-
-Result.prototype.cancelBatchExecution = function() {
-  const self = this;
-  self.statements_count(1);
-  self.hasMore(false);
-  self.statement_range({
-    start: {
-      row: 0,
-      column: 0
-    },
-    end: {
-      row: 0,
-      column: 0
-    }
-  });
-  self.handle()['statement_id'] = 0;
-  self.handle()['start'] = {
-    row: 0,
-    column: 0
-  };
-  self.handle()['end'] = {
-    row: 0,
-    column: 0
-  };
-  self.handle()['has_more_statements'] = false;
-  self.handle()['previous_statement_hash'] = '';
-};
-
-const getDefaultSnippetProperties = function(snippetType) {
-  const properties = {};
-
-  if (snippetType == 'jar' || snippetType == 'py') {
-    properties['driverCores'] = '';
-    properties['executorCores'] = '';
-    properties['numExecutors'] = '';
-    properties['queue'] = '';
-    properties['archives'] = [];
-    properties['files'] = [];
-  }
-
-  if (snippetType == 'java') {
-    properties['archives'] = [];
-    properties['files'] = [];
-    properties['capture_output'] = false;
-  }
-
-  if (snippetType == 'shell') {
-    properties['archives'] = [];
-    properties['files'] = [];
-  }
-
-  if (snippetType == 'mapreduce') {
-    properties['app_jar'] = '';
-    properties['hadoopProperties'] = [];
-    properties['jars'] = [];
-    properties['files'] = [];
-    properties['archives'] = [];
-  }
-
-  if (snippetType == 'spark2') {
-    properties['app_name'] = '';
-    properties['class'] = '';
-    properties['jars'] = [];
-    properties['spark_opts'] = [];
-    properties['spark_arguments'] = [];
-    properties['files'] = [];
-  }
-
-  if (snippetType == 'sqoop1') {
-    properties['files'] = [];
-  }
-
-  if (snippetType == 'jar' || snippetType == 'java') {
-    properties['app_jar'] = '';
-    properties['class'] = '';
-    properties['arguments'] = [];
-  } else if (snippetType == 'distcp') {
-    properties['source_path'] = '';
-    properties['destination_path'] = '';
-  } else if (snippetType == 'shell') {
-    properties['command_path'] = '';
-    properties['arguments'] = [];
-    properties['env_var'] = [];
-    properties['capture_output'] = true;
-  } else if (snippetType == 'py') {
-    properties['py_file'] = '';
-    properties['arguments'] = [];
-  } else if (snippetType == 'hive') {
-    properties['settings'] = [];
-    properties['files'] = [];
-    properties['functions'] = [];
-    properties['arguments'] = [];
-  } else if (snippetType == 'impala') {
-    properties['settings'] = [];
-  } else if (snippetType == 'pig') {
-    properties['parameters'] = [];
-    properties['hadoopProperties'] = [];
-    properties['resources'] = [];
-  }
-
-  return properties;
-};
-
-const ERROR_REGEX = /line ([0-9]+)(\:([0-9]+))?/i;
-
-const Snippet = function(vm, notebook, snippet) {
-  const self = this;
-
-  self.id = ko.observable(
-    typeof snippet.id != 'undefined' && snippet.id != null ? snippet.id : hueUtils.UUID()
-  );
-  self.name = ko.observable(
-    typeof snippet.name != 'undefined' && snippet.name != null ? snippet.name : ''
-  );
-  self.type = ko.observable(
-    typeof snippet.type != 'undefined' && snippet.type != null ? snippet.type : 'hive'
-  );
-  self.type.subscribe(newVal => {
-    self.status('ready');
-  });
-
-  self.isBatchable = ko.computed(() => {
-    return (
-      self.type() == 'hive' ||
-      self.type() == 'impala' ||
-      $.grep(vm.availableLanguages, language => {
-        return language.type == self.type() && language.interface == 'oozie';
-      }).length > 0
-    );
-  });
-
-  self.autocompleteSettings = {
-    temporaryOnly: false
-  };
-
-  // Ace stuff
-  self.aceCursorPosition = ko.observable(notebook.isHistory() ? snippet.aceCursorPosition : null);
-
-  let aceEditor = null;
-
-  self.ace = function(newVal) {
-    if (newVal) {
-      aceEditor = newVal;
-      if (!notebook.isPresentationMode()) {
-        aceEditor.focus();
-      }
-    }
-    return aceEditor;
-  };
-  self.errors = ko.observableArray([]);
-
-  self.aceErrorsHolder = ko.observableArray([]);
-  self.aceWarningsHolder = ko.observableArray([]);
-
-  self.aceErrors = ko.pureComputed(() => {
-    return self.showOptimizer() ? self.aceErrorsHolder() : [];
-  });
-  self.aceWarnings = ko.pureComputed(() => {
-    return self.showOptimizer() ? self.aceWarningsHolder() : [];
-  });
-
-  self.availableSnippets = vm.availableSnippets();
-  self.inFocus = ko.observable(false);
-
-  self.inFocus.subscribe(newValue => {
-    if (newValue) {
-      huePubSub.publish('active.snippet.type.changed', self.type());
-    }
-  });
-
-  self.editorMode = vm.editorMode;
-
-  self.getAceMode = function() {
-    return vm.getSnippetViewSettings(self.type()).aceMode;
-  };
-
-  self.dbSelectionVisible = ko.observable(false);
-
-  self.showExecutionAnalysis = ko.observable(false);
-
-  self.isSqlDialect = ko.pureComputed(() => {
-    return vm.getSnippetViewSettings(self.type()).sqlDialect;
-  });
-
-  self.getPlaceHolder = function() {
-    return vm.getSnippetViewSettings(self.type()).placeHolder;
-  };
-
-  self.getApiHelper = function() {
-    return apiHelper;
-  };
-
-  // namespace and compute might be initialized as empty object {}
-  self.namespace = ko.observable(
-    snippet.namespace && snippet.namespace.id ? snippet.namespace : undefined
-  );
-  self.compute = ko.observable(snippet.compute && snippet.compute.id ? snippet.compute : undefined);
-
-  self.whenContextSet = function() {
-    let namespaceSub;
-    const namespaceDeferred = $.Deferred();
-    if (self.namespace()) {
-      namespaceDeferred.resolve(self.namespace());
-    } else {
-      namespaceSub = self.namespace.subscribe(newVal => {
-        if (newVal) {
-          namespaceDeferred.resolve(newVal);
-          namespaceSub.dispose();
-        }
-      });
-    }
-    let computeSub;
-    const computeDeferred = $.Deferred();
-    if (self.compute()) {
-      computeDeferred.resolve(self.compute());
-    } else {
-      computeSub = self.compute.subscribe(newVal => {
-        if (newVal) {
-          computeDeferred.resolve(newVal);
-          computeSub.dispose();
-        }
-      });
-    }
-
-    const result = $.when(namespaceDeferred, computeDeferred);
-
-    result.dispose = function() {
-      if (namespaceSub) {
-        namespaceSub.dispose();
-      }
-      if (computeSub) {
-        computeSub.dispose();
-      }
-      namespaceDeferred.reject();
-      computeDeferred.reject();
-    };
-
-    return result;
-  };
-
-  self.availableDatabases = ko.observableArray();
-  self.database = ko.observable();
-  let previousDatabase = null;
-
-  self.database.subscribe(newValue => {
-    if (newValue !== null) {
-      apiHelper.setInTotalStorage('editor', 'last.selected.database', newValue);
-      if (previousDatabase !== null && previousDatabase !== newValue) {
-        huePubSub.publish('editor.refresh.statement.locations', self);
-      }
-      previousDatabase = newValue;
-    }
-  });
-
-  self.database(
-    typeof snippet.database !== 'undefined' && snippet.database != null ? snippet.database : null
-  );
-
-  // History is currently in Notebook, same with saved queries by snippets, might be better in assist
-  self.currentQueryTab = ko.observable(
-    typeof snippet.currentQueryTab != 'undefined' && snippet.currentQueryTab != null
-      ? snippet.currentQueryTab
-      : 'queryHistory'
-  );
-  self.pinnedContextTabs = ko.observableArray(
-    typeof snippet.pinnedContextTabs != 'undefined' && snippet.pinnedContextTabs != null
-      ? snippet.pinnedContextTabs
-      : []
-  );
-
-  self.removeContextTab = function(context) {
-    if (context.tabId === self.currentQueryTab()) {
-      self.currentQueryTab('queryHistory');
-    }
-    self.pinnedContextTabs.remove(context);
-  };
-
-  self.errorLoadingQueries = ko.observable(false);
-  self.loadingQueries = ko.observable(false);
-
-  self.queriesHasErrors = ko.observable(false);
-  self.queriesCurrentPage = ko.observable(
-    vm.selectedNotebook() && vm.selectedNotebook().snippets().length > 0
-      ? vm
-          .selectedNotebook()
-          .snippets()[0]
-          .queriesCurrentPage()
-      : 1
-  );
-  self.queriesTotalPages = ko.observable(
-    vm.selectedNotebook() && vm.selectedNotebook().snippets().length > 0
-      ? vm
-          .selectedNotebook()
-          .snippets()[0]
-          .queriesTotalPages()
-      : 1
-  );
-  self.queries = ko.observableArray([]);
-
-  self.queriesFilter = ko.observable('');
-  self.queriesFilterVisible = ko.observable(false);
-  self.queriesFilter.extend({ rateLimit: { method: 'notifyWhenChangesStop', timeout: 900 } });
-  self.queriesFilter.subscribe(val => {
-    self.fetchQueries();
-  });
-
-  let lastFetchQueriesRequest = null;
-
-  self.fetchQueries = function() {
-    apiHelper.cancelActiveRequest(lastFetchQueriesRequest);
-
-    const QUERIES_PER_PAGE = 50;
-    lastQueriesPage = self.queriesCurrentPage();
-    self.loadingQueries(true);
-    self.queriesHasErrors(false);
-    lastFetchQueriesRequest = apiHelper.searchDocuments({
-      successCallback: function(result) {
-        self.queriesTotalPages(Math.ceil(result.count / QUERIES_PER_PAGE));
-        self.queries(komapping.fromJS(result.documents)());
-        self.loadingQueries(false);
-        self.queriesHasErrors(false);
-      },
-      errorCallback: function() {
-        self.loadingQueries(false);
-        self.queriesHasErrors(true);
-      },
-      page: self.queriesCurrentPage(),
-      limit: QUERIES_PER_PAGE,
-      type: 'query-' + self.type(),
-      query: self.queriesFilter(),
-      include_trashed: false
-    });
-  };
-
-  let lastQueriesPage = 1;
-  self.currentQueryTab.subscribe(newValue => {
-    huePubSub.publish('redraw.fixed.headers');
-    huePubSub.publish('tab.switched', newValue);
-    if (
-      newValue === 'savedQueries' &&
-      (self.queries().length === 0 || lastQueriesPage !== self.queriesCurrentPage())
-    ) {
-      self.fetchQueries();
-    }
-  });
-
-  self.prevQueriesPage = function() {
-    if (self.queriesCurrentPage() !== 1) {
-      self.queriesCurrentPage(self.queriesCurrentPage() - 1);
-      self.fetchQueries();
-    }
-  };
-
-  self.nextQueriesPage = function() {
-    if (self.queriesCurrentPage() !== self.queriesTotalPages()) {
-      self.queriesCurrentPage(self.queriesCurrentPage() + 1);
-      self.fetchQueries();
-    }
-  };
-
-  huePubSub.subscribeOnce(
-    'assist.source.set',
-    source => {
-      if (source !== self.type()) {
-        huePubSub.publish('assist.set.source', self.type());
-      }
-    },
-    vm.huePubSubId
-  );
-
-  huePubSub.publish('assist.get.source');
-
-  let ignoreNextAssistDatabaseUpdate = false;
-  self.handleAssistSelection = function(databaseDef) {
-    if (ignoreNextAssistDatabaseUpdate) {
-      ignoreNextAssistDatabaseUpdate = false;
-    } else if (databaseDef.sourceType === self.type()) {
-      if (self.namespace() !== databaseDef.namespace) {
-        self.namespace(databaseDef.namespace);
-      }
-      if (self.database() !== databaseDef.name) {
-        self.database(databaseDef.name);
-      }
-    }
-  };
-
-  if (!self.database()) {
-    huePubSub.publish('assist.get.database.callback', {
-      source: self.type(),
-      callback: function(databaseDef) {
-        self.handleAssistSelection(databaseDef);
-      }
-    });
-  }
-
-  self.statementType = ko.observable(
-    typeof snippet.statementType != 'undefined' && snippet.statementType != null
-      ? snippet.statementType
-      : 'text'
-  );
-  self.statementTypes = ko.observableArray(['text', 'file']); // Maybe computed later for Spark
-  if (!vm.editorMode()) {
-    self.statementTypes.push('document');
-  }
-  self.statementPath = ko.observable(
-    typeof snippet.statementPath != 'undefined' && snippet.statementPath != null
-      ? snippet.statementPath
-      : ''
-  );
-  self.statementPath.subscribe(newVal => {
-    self.getExternalStatement();
-  });
-  self.externalStatementLoaded = ko.observable(false);
-  self.getExternalStatement = function() {
-    self.externalStatementLoaded(false);
-    $.post(
-      '/notebook/api/get_external_statement',
-      {
-        notebook: komapping.toJSON(notebook.getContext()),
-        snippet: komapping.toJSON(self.getContext())
-      },
-      data => {
-        if (data.status == 0) {
-          self.externalStatementLoaded(true);
-          self.statement_raw(data.statement);
-          self.ace().setValue(self.statement_raw(), 1);
-        } else {
-          self._ajaxError(data);
-        }
-      }
-    );
-  };
-  self.associatedDocumentLoading = ko.observable(true);
-  self.associatedDocument = ko.observable();
-  self.associatedDocumentUuid = ko.observable(
-    typeof snippet.associatedDocumentUuid != 'undefined' && snippet.associatedDocumentUuid != null
-      ? snippet.associatedDocumentUuid
-      : null
-  );
-  self.associatedDocumentUuid.subscribe(val => {
-    if (val !== '') {
-      self.getExternalStatement();
-    } else {
-      self.statement_raw('');
-      self.ace().setValue('', 1);
-    }
-  });
-  self.statement_raw = ko.observable(
-    typeof snippet.statement_raw != 'undefined' && snippet.statement_raw != null
-      ? snippet.statement_raw
-      : ''
-  );
-  self.selectedStatement = ko.observable('');
-  self.positionStatement = ko.observable(null);
-  self.lastExecutedStatement = ko.observable(null);
-  self.statementsList = ko.observableArray();
-
-  huePubSub.subscribe(
-    'editor.active.statement.changed',
-    statementDetails => {
-      if (self.ace() && self.ace().container.id === statementDetails.id) {
-        if (statementDetails.activeStatement) {
-          self.positionStatement(statementDetails.activeStatement);
-        } else {
-          self.positionStatement(null);
-        }
-
-        if (statementDetails.activeStatement) {
-          const _statements = [];
-          statementDetails.precedingStatements.forEach(statement => {
-            _statements.push(statement.statement);
-          });
-          _statements.push(statementDetails.activeStatement.statement);
-          statementDetails.followingStatements.forEach(statement => {
-            _statements.push(statement.statement);
-          });
-          self.statementsList(_statements); // Or fetch on demand via editor.refresh.statement.locations and remove observableArray?
-        } else {
-          self.statementsList([]);
-        }
-        if (!notebook.isPresentationModeInitialized()) {
-          if (notebook.isPresentationModeDefault()) {
-            // When switching to presentation mode, the snippet in non presentation mode cannot get status notification.
-            // On initiailization, status is set to loading and does not get updated, because we moved to presentation mode.
-            self.status('ready');
-          }
-          // Changing to presentation mode requires statementsList to be initialized. statementsList is initialized asynchronously.
-          // When presentation mode is default, we cannot change before statementsList has been calculated.
-          // Cleaner implementation would be to make toggleEditorMode statementsList asynchronous
-          // However this is currently impossible due to delete _notebook.presentationSnippets()[key];
-          notebook.isPresentationModeInitialized(true);
-          notebook.isPresentationMode(notebook.isPresentationModeDefault());
-        }
-      }
-    },
-    vm.huePubSubId
-  );
-
-  self.aceSize = ko.observable(
-    typeof snippet.aceSize != 'undefined' && snippet.aceSize != null ? snippet.aceSize : 100
-  );
-  // self.statement_raw.extend({ rateLimit: 150 }); // Should prevent lag from typing but currently send the old query when using the key shortcut
-  self.status = ko.observable(
-    typeof snippet.status != 'undefined' && snippet.status != null ? snippet.status : 'loading'
-  );
-  self.statusForButtons = ko.observable('executed');
-
-  self.properties = ko.observable(
-    komapping.fromJS(
-      typeof snippet.properties != 'undefined' && snippet.properties != null
-        ? snippet.properties
-        : getDefaultSnippetProperties(self.type())
-    )
-  );
-  self.hasProperties = ko.computed(() => {
-    return Object.keys(komapping.toJS(self.properties())).length > 0;
-  });
-
-  self.viewSettings = ko.computed(() => {
-    return vm.getSnippetViewSettings(self.type());
-  });
-
-  const previousProperties = {};
-  self.type.subscribe(
-    oldValue => {
-      previousProperties[oldValue] = self.properties();
-    },
-    null,
-    'beforeChange'
-  );
-
-  self.type.subscribe(newValue => {
-    if (typeof previousProperties[newValue] != 'undefined') {
-      self.properties(previousProperties[newValue]);
-    } else {
-      self.properties(komapping.fromJS(getDefaultSnippetProperties(newValue)));
-    }
-    self.result.clear();
-    window.setTimeout(() => {
-      if (self.ace() !== null) {
-        self.ace().focus();
-      }
-    }, 100);
-  });
-  if (snippet.variables) {
-    snippet.variables.forEach(variable => {
-      variable.meta = (typeof variable.defaultValue === 'object' && variable.defaultValue) || {
-        type: 'text',
-        placeholder: ''
-      };
-      variable.value = variable.value || '';
-      variable.type = variable.type || 'text';
-      variable.sample = [];
-      variable.sampleUser = variable.sampleUser || [];
-      variable.path = variable.path || '';
-      variable.step = '';
-      delete variable.defaultValue;
-    });
-  }
-  self.variables = komapping.fromJS(
-    typeof snippet.variables != 'undefined' && snippet.variables != null ? snippet.variables : []
-  );
-  self.variables.subscribe(newValue => {
-    $(document).trigger('updateResultHeaders', self);
-  });
-  self.hasCurlyBracketParameters = ko.computed(() => {
-    return self.type() != 'pig';
-  });
-  self.getPigParameters = function() {
-    const params = {};
-    const variables = self.statement_raw().match(/([^\\]|^)\$[^\d'"](\w*)/g);
-    const declares = self.statement_raw().match(/%declare +([^ ])+/gi);
-    const defaults = self.statement_raw().match(/%default +([^;])+/gi);
-    const macro_defines = self.statement_raw().match(/define [^ ]+ *\(([^\)]*)\)/gi); // no multiline
-    const macro_returns = self.statement_raw().match(/returns +([^\{]*)/gi); // no multiline
-
-    if (variables) {
-      $.each(variables, (index, param) => {
-        const p = param.substring(param.indexOf('$') + 1);
-        params[p] = '';
-      });
-    }
-    if (declares) {
-      $.each(declares, (index, param) => {
-        param = param.match(/(\w+)/g);
-        if (param && param.length >= 2) {
-          delete params[param[1]];
-        }
-      });
-    }
-    if (defaults) {
-      $.each(defaults, (index, param) => {
-        const line = param.match(/(\w+)/g);
-        if (line && line.length >= 2) {
-          const name = line[1];
-          params[name] = param.substring(param.indexOf(name) + name.length + 1);
-        }
-      });
-    }
-    if (macro_defines) {
-      $.each(macro_defines, (index, params_line) => {
-        const param_line = params_line.match(/(\w+)/g);
-        if (param_line && param_line.length > 2) {
-          $.each(param_line, (index, param) => {
-            if (index >= 2) {
-              // Skips define NAME
-              delete params[param];
-            }
-          });
-        }
-      });
-    }
-    if (macro_returns) {
-      $.each(macro_returns, (index, params_line) => {
-        const param_line = params_line.match(/(\w+)/g);
-        if (param_line) {
-          $.each(param_line, (index, param) => {
-            if (index >= 1) {
-              // Skip returns
-              delete params[param];
-            }
-          });
-        }
-      });
-    }
-
-    return params;
-  };
-  self.variableNames = ko.computed(() => {
-    let match,
-      matches = {},
-      matchList;
-    if (self.type() == 'pig') {
-      matches = self.getPigParameters();
-    } else {
-      const re = /(?:^|\W)\${(\w*)\=?([^{}]*)}/g;
-      const reComment = /(^\s*--.*)|(\/\*[\s\S]*?\*\/)/gm;
-      const reList = /(?!\s*$)\s*(?:(?:([^,|()\\]*)\(\s*([^,|()\\]*)\)(?:\\[\S\s][^,|()\\]*)?)|([^,|\\]*(?:\\[\S\s][^,|\\]*)*))\s*(?:,|\||$)/g;
-      const statement = self.statement_raw();
-      let matchComment = reComment.exec(statement);
-      // if re is n & reComment is m
-      // finding variables is O(n+m)
-      while ((match = re.exec(statement))) {
-        while (matchComment && match.index > matchComment.index + matchComment[0].length) {
-          // Comments before our match
-          matchComment = reComment.exec(statement);
-        }
-        const isWithinComment = matchComment && match.index >= matchComment.index;
-        if (isWithinComment) {
-          continue;
-        }
-
-        // If 1 match, text value
-        // If multiple matches, list value
-        const value = { type: 'text', placeholder: '' };
-        while ((matchList = reList.exec(match[2]))) {
-          const option = {
-            text: matchList[2] || matchList[3],
-            value: matchList[3] || matchList[1]
-          };
-          option.text = option.text && option.text.trim();
-          option.value =
-            option.value &&
-            option.value
-              .trim()
-              .replace(',', ',')
-              .replace('(', '(')
-              .replace(')', ')');
-
-          if (value.placeholder || matchList[2]) {
-            if (!value.options) {
-              value.options = [];
-              value.type = 'select';
-            }
-            value.options.push(option);
-          }
-          if (!value.placeholder) {
-            value.placeholder = option.value;
-          }
-        }
-        const isPlaceholderInOptions =
-          !value.options ||
-          value.options.some(current => {
-            return current.value == value.placeholder;
-          });
-        if (!isPlaceholderInOptions) {
-          value.options.unshift({ text: value.placeholder, value: value.placeholder });
-        }
-        matches[match[1]] = matches[match[1]] || value;
-      }
-    }
-    return $.map(matches, (match, key) => {
-      const isMatchObject = typeof matches[key] === 'object';
-      const meta = isMatchObject ? matches[key] : { type: 'text', placeholder: matches[key] };
-      return { name: key, meta: meta };
-    });
-  });
-  self.variableValues = {};
-  self.variableNames.extend({ rateLimit: 150 });
-  self.variableNames.subscribe(newVal => {
-    const variablesLength = self.variables().length;
-    const diffLengthVariables = variablesLength - newVal.length;
-    const needsMore = diffLengthVariables < 0;
-    const needsLess = diffLengthVariables > 0;
-    self.variableValues = self.variables().reduce((variableValues, variable) => {
-      if (!variableValues[variable.name()]) {
-        variableValues[variable.name()] = { sampleUser: [] };
-      }
-      variableValues[variable.name()].value = variable.value();
-      variableValues[variable.name()].sampleUser = variable.sampleUser();
-      variableValues[variable.name()].catalogEntry = variable.catalogEntry;
-      variableValues[variable.name()].path = variable.path();
-      variableValues[variable.name()].type = variable.type();
-      return variableValues;
-    }, self.variableValues);
-    if (needsMore) {
-      for (let i = 0, length = Math.abs(diffLengthVariables); i < length; i++) {
-        self.variables.push(
-          komapping.fromJS({
-            name: '',
-            value: '',
-            meta: { type: 'text', placeholder: '', options: [] },
-            sample: [],
-            sampleUser: [],
-            type: 'text',
-            step: '',
-            path: ''
-          })
-        );
-      }
-    } else if (needsLess) {
-      self.variables.splice(self.variables().length - diffLengthVariables, diffLengthVariables);
-    }
-    newVal.forEach((item, index) => {
-      const variable = self.variables()[index];
-      variable.name(item.name);
-      setTimeout(() => {
-        variable.value(
-          self.variableValues[item.name]
-            ? self.variableValues[item.name].value
-            : (!needsMore && variable.value()) || ''
-        );
-      }, 0);
-      variable.meta = komapping.fromJS(item.meta, {}, variable.meta);
-      variable.sample(
-        variable.meta.options
-          ? variable.meta.options().concat(variable.sampleUser())
-          : variable.sampleUser()
-      );
-      variable.sampleUser(
-        self.variableValues[item.name] ? self.variableValues[item.name].sampleUser : []
-      );
-      variable.type(
-        self.variableValues[item.name] ? self.variableValues[item.name].type || 'text' : 'text'
-      );
-      variable.path(
-        self.variableValues[item.name] ? self.variableValues[item.name].path || '' : ''
-      );
-      variable.catalogEntry =
-        self.variableValues[item.name] && self.variableValues[item.name].catalogEntry;
-    });
-  });
-
-  const activeSourcePromises = [];
-  huePubSub.subscribe('ace.sql.location.worker.message', e => {
-    while (activeSourcePromises.length) {
-      const promise = activeSourcePromises.pop();
-      if (promise.cancel) {
-        promise.cancel();
-      }
-    }
-    const oLocations = e.data.locations
-      .filter(location => {
-        return location.type === 'variable' && location.colRef;
-      })
-      .reduce((variables, location) => {
-        const re = /\${(\w*)\=?([^{}]*)}/g;
-        const name = re.exec(location.value)[1];
-        variables[name] = location;
-        return variables;
-      }, {});
-    const updateVariableType = function(variable, sourceMeta) {
-      let type;
-      if (sourceMeta && sourceMeta.type) {
-        type = sourceMeta.type.toLowerCase();
-      } else {
-        type = 'string';
-      }
-      const variablesValues = {};
-      const value = variable.value();
-      switch (type) {
-        case 'timestamp':
-          variablesValues.type = 'datetime-local';
-          variablesValues.step = '1';
-          variablesValues.value =
-            (value && moment.utc(value).format('YYYY-MM-DD HH:mm:ss.S')) ||
-            moment(Date.now()).format('YYYY-MM-DD 00:00:00.0');
-          break;
-        case 'decimal':
-        case 'double':
-        case 'float':
-          variablesValues.type = 'number';
-          variablesValues.step = 'any';
-          break;
-        case 'int':
-        case 'smallint':
-        case 'tinyint':
-        case 'bigint':
-          variablesValues.type = 'number';
-          variablesValues.step = '1';
-          break;
-        case 'date':
-          variablesValues.type = 'date';
-          variablesValues.step = '';
-          variablesValues.value =
-            (value && moment.utc(value).format('YYYY-MM-DD')) ||
-            moment(Date.now()).format('YYYY-MM-DD');
-          break;
-        case 'boolean':
-          variablesValues.type = 'checkbox';
-          variablesValues.step = '';
-          break;
-        default:
-          variablesValues.type = 'text';
-          variablesValues.step = '';
-      }
-      if (variablesValues.value) {
-        setTimeout(() => {
-          variable.value(variablesValues.value);
-        }, 0);
-      }
-      variable.type(variablesValues.type);
-      variable.step(variablesValues.step);
-    };
-    self.variables().forEach(variable => {
-      if (oLocations[variable.name()]) {
-        activeSourcePromises.push(
-          oLocations[variable.name()].resolveCatalogEntry({ cancellable: true }).done(entry => {
-            variable.path(entry.path.join('.'));
-            variable.catalogEntry = entry;
-
-            activeSourcePromises.push(
-              entry
-                .getSourceMeta({
-                  silenceErrors: true,
-                  cancellable: true
-                })
-                .then(updateVariableType.bind(self, variable))
-            );
-          })
-        );
-      } else {
-        updateVariableType(variable, {
-          type: 'text'
-        });
-      }
-    });
-  });
-  self.statement = ko.computed(() => {
-    let statement = self.isSqlDialect()
-      ? self.selectedStatement()
-        ? self.selectedStatement()
-        : self.positionStatement() !== null
-        ? self.positionStatement().statement
-        : self.statement_raw()
-      : self.statement_raw();
-    const variables = self.variables().reduce((variables, variable) => {
-      variables[variable.name()] = variable;
-      return variables;
-    }, {});
-    if (self.variables().length) {
-      const variablesString = self
-        .variables()
-        .map(variable => {
-          return variable.name();
-        })
-        .join('|');
-      statement = statement.replace(
-        RegExp(
-          '([^\\\\])?\\$' +
-            (self.hasCurlyBracketParameters() ? '{(' : '(') +
-            variablesString +
-            ')(=[^}]*)?' +
-            (self.hasCurlyBracketParameters() ? '}' : ''),
-          'g'
-        ),
-        (match, p1, p2) => {
-          const variable = variables[p2];
-          const pad =
-            variable.type() == 'datetime-local' && variable.value().length == 16 ? ':00' : ''; // Chrome drops the seconds from the timestamp when it's at 0 second.
-          const value = variable.value();
-          return (
-            p1 +
-            (value !== undefined && value !== null
-              ? value + pad
-              : variable.meta.placeholder && variable.meta.placeholder())
-          );
-        }
-      );
-    }
-    return statement;
-  });
-
-  self.result = new Result(snippet, snippet.result);
-  if (!self.result.hasSomeResults()) {
-    self.currentQueryTab('queryHistory');
-  }
-  self.showGrid = ko.observable(
-    typeof snippet.showGrid != 'undefined' && snippet.showGrid != null ? snippet.showGrid : true
-  );
-  self.showChart = ko.observable(
-    typeof snippet.showChart != 'undefined' && snippet.showChart != null ? snippet.showChart : false
-  );
-  let defaultShowLogs = true;
-  if (vm.editorMode() && $.totalStorage('hue.editor.showLogs')) {
-    defaultShowLogs = $.totalStorage('hue.editor.showLogs');
-  }
-  self.showLogs = ko.observable(
-    typeof snippet.showLogs !== 'undefined' && snippet.showLogs != null
-      ? snippet.showLogs
-      : defaultShowLogs
-  );
-  self.progress = ko.observable(
-    typeof snippet.progress !== 'undefined' && snippet.progress != null ? snippet.progress : 0
-  );
-  self.jobs = ko.observableArray(
-    typeof snippet.jobs !== 'undefined' && snippet.jobs != null ? snippet.jobs : []
-  );
-
-  self.executeNextTimeout = -1;
-  const refreshTimeouts = {};
-  self.onDdlExecute = function() {
-    if (self.result.handle() && self.result.handle().has_more_statements) {
-      window.clearTimeout(self.executeNextTimeout);
-      self.executeNextTimeout = setTimeout(() => {
-        self.execute(true); // Execute next, need to wait as we disabled fast click
-      }, 1000);
-    }
-    if (
-      self.lastExecutedStatement() &&
-      /CREATE|DROP/i.test(self.lastExecutedStatement().firstToken)
-    ) {
-      let match = self
-        .statement()
-        .match(
-          /(?:CREATE|DROP)\s+TABLE\s+(?:IF\s+(?:NOT\s+)?EXISTS\s+)?(?:`([^`]+)`|([^;\s]+))\..*/i
-        );
-      const path = [];
-      if (match) {
-        path.push(match[1] || match[2]); // group 1 backticked db name, group 2 regular db name
-      } else {
-        match = self
-          .statement()
-          .match(
-            /(?:CREATE|DROP)\s+(?:DATABASE|SCHEMA)\s+(?:IF\s+(?:NOT\s+)?EXISTS\s+)?(?:`([^`]+)`|([^;\s]+))/i
-          );
-        if (match) {
-          path.push(match[1] || match[2]); // group 1 backticked db name, group 2 regular db name
-        } else if (self.database()) {
-          path.push(self.database());
-        }
-      }
-
-      if (path.length) {
-        window.clearTimeout(refreshTimeouts[path.join('.')]);
-        refreshTimeouts[path.join('.')] = window.setTimeout(() => {
-          ignoreNextAssistDatabaseUpdate = true;
-          dataCatalog
-            .getEntry({
-              sourceType: self.type(),
-              namespace: self.namespace(),
-              compute: self.compute(),
-              path: path
-            })
-            .done(entry => {
-              entry.clearCache({ invalidate: 'invalidate', cascade: true, silenceErrors: true });
-            });
-        }, 5000);
-      }
-    }
-  };
-
-  self.progress.subscribe(val => {
-    $(document).trigger('progress', { data: val, snippet: self });
-  });
-
-  self.showGrid.subscribe(val => {
-    if (val) {
-      self.showChart(false);
-      huePubSub.publish('editor.grid.shown', self);
-    }
-  });
-
-  function prepopulateChart() {
-    const type = self.chartType();
-    hueAnalytics.log('notebook', 'chart/' + type);
-
-    if (type === ko.HUE_CHARTS.TYPES.MAP && self.result.cleanedNumericMeta().length >= 2) {
-      if (self.chartX() === null || typeof self.chartX() === 'undefined') {
-        let name = self.result.cleanedNumericMeta()[0].name;
-        self.result.cleanedNumericMeta().forEach(fld => {
-          if (
-            fld.name.toLowerCase().indexOf('lat') > -1 ||
-            fld.name.toLowerCase().indexOf('ltd') > -1
-          ) {
-            name = fld.name;
-          }
-        });
-        self.chartX(name);
-      }
-      if (self.chartYSingle() === null || typeof self.chartYSingle() === 'undefined') {
-        let name = self.result.cleanedNumericMeta()[1].name;
-        self.result.cleanedNumericMeta().forEach(fld => {
-          if (
-            fld.name.toLowerCase().indexOf('lon') > -1 ||
-            fld.name.toLowerCase().indexOf('lng') > -1
-          ) {
-            name = fld.name;
-          }
-        });
-        self.chartYSingle(name);
-      }
-      return;
-    }
-
-    if (
-      (self.chartX() === null || typeof self.chartX() === 'undefined') &&
-      (type == ko.HUE_CHARTS.TYPES.BARCHART ||
-        type == ko.HUE_CHARTS.TYPES.PIECHART ||
-        type == ko.HUE_CHARTS.TYPES.GRADIENTMAP) &&
-      self.result.cleanedStringMeta().length >= 1
-    ) {
-      self.chartX(self.result.cleanedStringMeta()[0].name);
-    }
-
-    if (self.result.cleanedNumericMeta().length > 0) {
-      if (
-        self.chartYMulti().length === 0 &&
-        (type === ko.HUE_CHARTS.TYPES.BARCHART || type === ko.HUE_CHARTS.TYPES.LINECHART)
-      ) {
-        self.chartYMulti.push(
-          self.result.cleanedNumericMeta()[Math.min(self.result.cleanedNumericMeta().length - 1, 1)]
-            .name
-        );
-      } else if (
-        (self.chartYSingle() === null || typeof self.chartYSingle() === 'undefined') &&
-        (type === ko.HUE_CHARTS.TYPES.PIECHART ||
-          type === ko.HUE_CHARTS.TYPES.MAP ||
-          type === ko.HUE_CHARTS.TYPES.GRADIENTMAP ||
-          type === ko.HUE_CHARTS.TYPES.SCATTERCHART ||
-          (type === ko.HUE_CHARTS.TYPES.BARCHART && self.chartXPivot() !== null))
-      ) {
-        if (self.chartYMulti().length === 0) {
-          self.chartYSingle(
-            self.result.cleanedNumericMeta()[
-              Math.min(self.result.cleanedNumericMeta().length - 1, 1)
-            ].name
-          );
-        } else {
-          self.chartYSingle(self.chartYMulti()[0]);
-        }
-      }
-    }
-  }
-
-  self.showChart.subscribe(val => {
-    if (val) {
-      self.showGrid(false);
-      self.isResultSettingsVisible(true);
-      $(document).trigger('forceChartDraw', self);
-      huePubSub.publish('editor.chart.shown', self);
-      prepopulateChart();
-    }
-  });
-  self.showLogs.subscribe(val => {
-    huePubSub.publish('redraw.fixed.headers');
-    if (val) {
-      self.getLogs();
-    }
-    if (vm.editorMode()) {
-      $.totalStorage('hue.editor.showLogs', val);
-    }
-  });
-
-  self.isLoading = ko.computed(() => {
-    return self.status() == 'loading';
-  });
-
-  self.resultsKlass = ko.computed(() => {
-    return 'results ' + self.type();
-  });
-
-  self.errorsKlass = ko.computed(() => {
-    return self.resultsKlass() + ' alert alert-error';
-  });
-
-  self.is_redacted = ko.observable(
-    typeof snippet.is_redacted != 'undefined' && snippet.is_redacted != null
-      ? snippet.is_redacted
-      : false
-  );
-
-  self.chartType = ko.observable(
-    typeof snippet.chartType != 'undefined' && snippet.chartType != null
-      ? snippet.chartType
-      : ko.HUE_CHARTS.TYPES.BARCHART
-  );
-  self.chartType.subscribe(prepopulateChart);
-  self.chartSorting = ko.observable(
-    typeof snippet.chartSorting != 'undefined' && snippet.chartSorting != null
-      ? snippet.chartSorting
-      : 'none'
-  );
-  self.chartScatterGroup = ko.observable(
-    typeof snippet.chartScatterGroup != 'undefined' && snippet.chartScatterGroup != null
-      ? snippet.chartScatterGroup
-      : null
-  );
-  self.chartScatterSize = ko.observable(
-    typeof snippet.chartScatterSize != 'undefined' && snippet.chartScatterSize != null
-      ? snippet.chartScatterSize
-      : null
-  );
-  self.chartScope = ko.observable(
-    typeof snippet.chartScope != 'undefined' && snippet.chartScope != null
-      ? snippet.chartScope
-      : 'world'
-  );
-  self.chartTimelineType = ko.observable(
-    typeof snippet.chartTimelineType != 'undefined' && snippet.chartTimelineType != null
-      ? snippet.chartTimelineType
-      : 'bar'
-  );
-  self.chartLimits = ko.observableArray([5, 10, 25, 50, 100]);
-  self.chartLimit = ko.observable(
-    typeof snippet.chartLimit != 'undefined' && snippet.chartLimit != null
-      ? snippet.chartLimit
-      : null
-  );
-  self.chartLimit.extend({ notify: 'always' });
-  self.chartX = ko.observable(
-    typeof snippet.chartX != 'undefined' && snippet.chartX != null ? snippet.chartX : null
-  );
-  self.chartX.extend({ notify: 'always' });
-  self.chartXPivot = ko.observable(
-    typeof snippet.chartXPivot != 'undefined' && snippet.chartXPivot != null
-      ? snippet.chartXPivot
-      : null
-  );
-  self.chartXPivot.extend({ notify: 'always' });
-  self.chartXPivot.subscribe(prepopulateChart);
-  self.chartYSingle = ko.observable(
-    typeof snippet.chartYSingle != 'undefined' && snippet.chartYSingle != null
-      ? snippet.chartYSingle
-      : null
-  );
-  self.chartYMulti = ko.observableArray(
-    typeof snippet.chartYMulti != 'undefined' && snippet.chartYMulti != null
-      ? snippet.chartYMulti
-      : []
-  );
-  self.chartData = ko.observableArray(
-    typeof snippet.chartData != 'undefined' && snippet.chartData != null ? snippet.chartData : []
-  );
-  self.chartMapType = ko.observable(
-    typeof snippet.chartMapType != 'undefined' && snippet.chartMapType != null
-      ? snippet.chartMapType
-      : 'marker'
-  );
-  self.chartMapLabel = ko.observable(
-    typeof snippet.chartMapLabel != 'undefined' && snippet.chartMapLabel != null
-      ? snippet.chartMapLabel
-      : null
-  );
-  self.chartMapHeat = ko.observable(
-    typeof snippet.chartMapHeat != 'undefined' && snippet.chartMapHeat != null
-      ? snippet.chartMapHeat
-      : null
-  );
-  self.hideStacked = ko.computed(() => {
-    return self.chartYMulti().length <= 1;
-  });
-
-  self.hasDataForChart = ko.computed(() => {
-    if (
-      self.chartType() == ko.HUE_CHARTS.TYPES.BARCHART ||
-      self.chartType() == ko.HUE_CHARTS.TYPES.LINECHART ||
-      self.chartType() == ko.HUE_CHARTS.TYPES.TIMELINECHART
-    ) {
-      return (
-        typeof self.chartX() != 'undefined' &&
-        self.chartX() != null &&
-        self.chartYMulti().length > 0
-      );
-    }
-    return (
-      typeof self.chartX() != 'undefined' &&
-      self.chartX() != null &&
-      typeof self.chartYSingle() != 'undefined' &&
-      self.chartYSingle() != null
-    );
-  });
-
-  self.hasDataForChart.subscribe(newValue => {
-    self.chartX.notifySubscribers();
-    self.chartX.valueHasMutated();
-  });
-
-  self.chartType.subscribe(val => {
-    $(document).trigger('forceChartDraw', self);
-  });
-
-  self.previousChartOptions = {};
-
-  function guessMetaField(field) {
-    let _fld = null;
-    if (field) {
-      if (self.result.cleanedMeta().length > 0) {
-        self.result.cleanedMeta().forEach(fld => {
-          if (
-            fld.name.toLowerCase() === field.toLowerCase() ||
-            field.toLowerCase() === fld.name.toLowerCase()
-          ) {
-            _fld = fld.name;
-          }
-        });
-      }
-    }
-    return _fld;
-  }
-
-  function guessMetaFields(fields) {
-    const _fields = [];
-    if (fields) {
-      fields.forEach(fld => {
-        const _field = guessMetaField(fld);
-        if (_field) {
-          _fields.push(_field);
-        }
-      });
-    }
-    return _fields;
-  }
-
-  self.result.meta.subscribe(newValue => {
-    self.chartLimit(self.previousChartOptions.chartLimit);
-    self.chartX(guessMetaField(self.previousChartOptions.chartX));
-    self.chartXPivot(self.previousChartOptions.chartXPivot);
-    self.chartYSingle(guessMetaField(self.previousChartOptions.chartYSingle));
-    self.chartMapType(self.previousChartOptions.chartMapType);
-    self.chartMapLabel(guessMetaField(self.previousChartOptions.chartMapLabel));
-    self.chartMapHeat(self.previousChartOptions.chartMapHeat);
-    self.chartYMulti(guessMetaFields(self.previousChartOptions.chartYMulti) || []);
-    self.chartSorting(self.previousChartOptions.chartSorting);
-    self.chartScatterGroup(self.previousChartOptions.chartScatterGroup);
-    self.chartScatterSize(self.previousChartOptions.chartScatterSize);
-    self.chartScope(self.previousChartOptions.chartScope);
-    self.chartTimelineType(self.previousChartOptions.chartTimelineType);
-  });
-
-  self.isResultSettingsVisible = ko.observable(
-    typeof snippet.isResultSettingsVisible != 'undefined' && snippet.isResultSettingsVisible != null
-      ? snippet.isResultSettingsVisible
-      : false
-  );
-  self.toggleResultSettings = function() {
-    self.isResultSettingsVisible(!self.isResultSettingsVisible());
-  };
-  self.isResultSettingsVisible.subscribe(() => {
-    $(document).trigger('toggleResultSettings', self);
-  });
-
-  self.settingsVisible = ko.observable(
-    typeof snippet.settingsVisible != 'undefined' && snippet.settingsVisible != null
-      ? snippet.settingsVisible
-      : false
-  );
-  self.saveResultsModalVisible = ko.observable(false);
-
-  self.checkStatusTimeout = null;
-
-  self.getContext = function() {
-    return {
-      id: self.id,
-      type: self.type,
-      status: self.status,
-      statementType: self.statementType,
-      statement: self.statement,
-      aceCursorPosition: self.aceCursorPosition,
-      statementPath: self.statementPath,
-      associatedDocumentUuid: self.associatedDocumentUuid,
-      properties: self.properties,
-      result: self.result.getContext(),
-      database: self.database,
-      compute: self.compute(),
-      wasBatchExecuted: self.wasBatchExecuted()
-    };
-  };
-
-  self.complexity = ko.observable();
-  self.hasComplexity = ko.pureComputed(() => {
-    return self.complexity() && Object.keys(self.complexity()).length > 0;
-  });
-  self.hasRisks = ko.pureComputed(() => {
-    return (
-      self.hasComplexity() && self.complexity()['hints'] && self.complexity()['hints'].length > 0
-    );
-  });
-  self.topRisk = ko.pureComputed(() => {
-    if (self.hasRisks()) {
-      return self.complexity()['hints'][0];
-    } else {
-      return null;
-    }
-  });
-
-  self.suggestion = ko.observable('');
-  self.hasSuggestion = ko.observable(null);
-
-  self.compatibilityCheckRunning = ko.observable(false);
-
-  self.compatibilitySourcePlatforms = [];
-  Object.keys(COMPATIBILITY_SOURCE_PLATFORMS).forEach(key => {
-    self.compatibilitySourcePlatforms.push(COMPATIBILITY_SOURCE_PLATFORMS[key]);
-  });
-
-  self.compatibilitySourcePlatform = ko.observable(COMPATIBILITY_SOURCE_PLATFORMS[self.type()]);
-  self.compatibilitySourcePlatform.subscribe(newValue => {
-    if (newValue && newValue.value !== self.type()) {
-      self.hasSuggestion(null);
-      self.compatibilityTargetPlatform(COMPATIBILITY_TARGET_PLATFORMS[self.type()]);
-      self.queryCompatibility();
-    }
-  });
-
-  self.compatibilityTargetPlatforms = [];
-  Object.keys(COMPATIBILITY_TARGET_PLATFORMS).forEach(key => {
-    self.compatibilityTargetPlatforms.push(COMPATIBILITY_TARGET_PLATFORMS[key]);
-  });
-  self.compatibilityTargetPlatform = ko.observable(COMPATIBILITY_TARGET_PLATFORMS[self.type()]);
-
-  self.showOptimizer = ko.observable(
-    apiHelper.getFromTotalStorage('editor', 'show.optimizer', false)
-  );
-  self.showOptimizer.subscribe(newValue => {
-    if (newValue !== null) {
-      apiHelper.setInTotalStorage('editor', 'show.optimizer', newValue);
-    }
-  });
-
-  if (HAS_OPTIMIZER && !vm.isNotificationManager()) {
-    let lastComplexityRequest;
-    let lastCheckedComplexityStatement;
-    const knownResponses = [];
-
-    self.delayedStatement = ko
-      .pureComputed(self.statement)
-      .extend({ rateLimit: { method: 'notifyWhenChangesStop', timeout: 2000 } });
-
-    const handleRiskResponse = function(data) {
-      if (data.status == 0) {
-        self.hasSuggestion('');
-        self.complexity(data.query_complexity);
-      } else {
-        self.hasSuggestion('error');
-        self.complexity({ hints: [] });
-      }
-      huePubSub.publish('editor.active.risks', {
-        editor: self.ace(),
-        risks: self.complexity() || {}
-      });
-      lastCheckedComplexityStatement = self.statement();
-    };
-
-    const clearActiveRisks = function() {
-      if (self.hasSuggestion() !== null && typeof self.hasSuggestion() !== 'undefined') {
-        self.hasSuggestion(null);
-      }
-
-      if (self.suggestion() !== '') {
-        self.suggestion('');
-      }
-
-      if (self.complexity() !== {}) {
-        self.complexity(undefined);
-        huePubSub.publish('editor.active.risks', {
-          editor: self.ace(),
-          risks: {}
-        });
-      }
-    };
-
-    self.positionStatement.subscribe(newStatement => {
-      if (newStatement) {
-        const hash = newStatement.statement.hashCode();
-        const unknownResponse = knownResponses.every(knownResponse => {
-          if (knownResponse.hash === hash) {
-            handleRiskResponse(knownResponse.data);
-            return false;
-          }
-          return true;
-        });
-        if (unknownResponse) {
-          clearActiveRisks();
-        }
-      }
-    });
-
-    self.checkComplexity = function() {
-      if (!self.inFocus() || lastCheckedComplexityStatement === self.statement()) {
-        return;
-      }
-
-      // The syntaxError property is only set if the syntax checker is active and has found an
-      // error, see AceLocationHandler.
-      if (self.positionStatement() && self.positionStatement().syntaxError) {
-        return;
-      }
-
-      apiHelper.cancelActiveRequest(lastComplexityRequest);
-
-      hueAnalytics.log('notebook', 'get_query_risk');
-      clearActiveRisks();
-
-      const changeSubscription = self.statement.subscribe(() => {
-        changeSubscription.dispose();
-        apiHelper.cancelActiveRequest(lastComplexityRequest);
-      });
-
-      const hash = self.statement().hashCode();
-
-      const unknownResponse = knownResponses.every(knownResponse => {
-        if (knownResponse.hash === hash) {
-          handleRiskResponse(knownResponse.data);
-          return false;
-        }
-        return true;
-      });
-      if (unknownResponse) {
-        lastComplexityRequest = $.ajax({
-          type: 'POST',
-          url: '/notebook/api/optimizer/statement/risk',
-          timeout: 30000, // 30 seconds
-          data: {
-            notebook: komapping.toJSON(notebook.getContext()),
-            snippet: komapping.toJSON(self.getContext())
-          },
-          success: function(data) {
-            knownResponses.unshift({
-              hash: hash,
-              data: data
-            });
-            if (knownResponses.length > 50) {
-              knownResponses.pop();
-            }
-            handleRiskResponse(data);
-          },
-          always: function(data) {
-            changeSubscription.dispose();
-          }
-        });
-      }
-    };
-
-    if (self.type() === 'hive' || self.type() === 'impala') {
-      if (self.statement_raw()) {
-        window.setTimeout(() => {
-          self.checkComplexity();
-        }, 2000);
-      }
-      self.delayedStatement.subscribe(() => {
-        self.checkComplexity();
-      });
-    }
-  }
-
-  self._ajaxError = function(data, callback) {
-    if (data.status == -2) {
-      // Session expired
-      const existingSession = notebook.getSession(self.type());
-      if (existingSession) {
-        notebook.restartSession(existingSession, callback);
-      } else {
-        notebook.createSession(new Session(vm, { type: self.type() }), callback);
-      }
-    } else if (data.status == -3) {
-      // Statement expired
-      self.status('expired');
-      if (data.message) {
-        self.errors.push({ message: data.message, help: null, line: null, col: null });
-        huePubSub.publish('editor.snippet.result.normal', self);
-      }
-    } else if (data.status == -4) {
-      // Operation timed out
-      notebook.retryModalCancel = function() {
-        self.status('failed');
-        huePubSub.publish('hide.retry.modal');
-      };
-      notebook.retryModalConfirm = function() {
-        if (callback) {
-          callback();
-        }
-        huePubSub.publish('hide.retry.modal');
-      };
-      huePubSub.publish('show.retry.modal');
-    } else if (data.status == 401) {
-      // Auth required
-      self.status('expired');
-      $(document).trigger('showAuthModal', { type: self.type(), callback: self.execute });
-    } else if (data.status == 1 || data.status == -1) {
-      self.status('failed');
-      const match = ERROR_REGEX.exec(data.message);
-      if (match) {
-        let errorLine = parseInt(match[1]);
-        let errorCol;
-        if (typeof match[3] !== 'undefined') {
-          errorCol = parseInt(match[3]);
-        }
-        if (self.positionStatement()) {
-          if (errorCol && errorLine === 1) {
-            errorCol += self.positionStatement().location.first_column;
-          }
-          errorLine += self.positionStatement().location.first_line - 1;
-        }
-
-        self.errors.push({
-          message: data.message.replace(
-            match[0],
-            'line ' + errorLine + (errorCol !== null ? ':' + errorCol : '')
-          ),
-          help: null,
-          line: errorLine - 1,
-          col: errorCol
-        });
-      } else {
-        self.errors.push({
-          message: data.message,
-          help: data.help,
-          line: null,
-          col: null
-        });
-      }
-    } else {
-      $(document).trigger('error', data.message);
-      self.status('failed');
-    }
-  };
-
-  self.wasBatchExecuted = ko.observable(
-    typeof snippet.wasBatchExecuted != 'undefined' && snippet.wasBatchExecuted != null
-      ? snippet.wasBatchExecuted
-      : false
-  );
-  self.isReady = ko.computed(() => {
-    return (
-      (self.statementType() == 'text' &&
-        ((self.isSqlDialect() && self.statement() !== '') ||
-          (['jar', 'java', 'spark2', 'distcp'].indexOf(self.type()) == -1 &&
-            self.statement() !== '') ||
-          (['jar', 'java'].indexOf(self.type()) != -1 &&
-            (self.properties().app_jar() != '' && self.properties().class() != '')) ||
-          (['spark2'].indexOf(self.type()) != -1 && self.properties().jars().length > 0) ||
-          (['shell'].indexOf(self.type()) != -1 && self.properties().command_path().length > 0) ||
-          (['mapreduce'].indexOf(self.type()) != -1 && self.properties().app_jar().length > 0) ||
-          (['distcp'].indexOf(self.type()) != -1 &&
-            self.properties().source_path().length > 0 &&
-            self.properties().destination_path().length > 0))) ||
-      (self.statementType() == 'file' && self.statementPath().length > 0) ||
-      (self.statementType() == 'document' &&
-        self.associatedDocumentUuid() &&
-        self.associatedDocumentUuid().length > 0)
-    );
-  });
-  self.lastExecuted = ko.observable(
-    typeof snippet.lastExecuted != 'undefined' && snippet.lastExecuted != null
-      ? snippet.lastExecuted
-      : 0
-  );
-  self.lastAceSelectionRowOffset = ko.observable(snippet.lastAceSelectionRowOffset || 0);
-
-  self.executingBlockingOperation = null; // A ExecuteStatement()
-  self.showLongOperationWarning = ko.observable(false);
-  self.showLongOperationWarning.subscribe(newValue => {
-    if (newValue) {
-      hueAnalytics.convert('editor', 'showLongOperationWarning');
-    }
-  });
-
-  let longOperationTimeout = -1;
-
-  function startLongOperationTimeout() {
-    longOperationTimeout = window.setTimeout(() => {
-      self.showLongOperationWarning(true);
-    }, 2000);
-  }
-
-  function stopLongOperationTimeout() {
-    window.clearTimeout(longOperationTimeout);
-    self.showLongOperationWarning(false);
-  }
-
-  self.lastExecutedStatements = undefined;
-  self.lastExecutedSelectionRange = undefined;
-
-  self.execute = function(automaticallyTriggered) {
-    if (!automaticallyTriggered && self.ace()) {
-      const selectionRange = self.ace().getSelectionRange();
-
-      if (
-        self.lastExecutedSelectionRange &&
-        (selectionRange.start.row !== selectionRange.end.row &&
-          selectionRange.start.column !== selectionRange.end.column) &&
-        (selectionRange.start.row !== self.lastExecutedSelectionRange.start.row ||
-          selectionRange.start.column !== self.lastExecutedSelectionRange.start.column ||
-          selectionRange.end.row !== self.lastExecutedSelectionRange.end.row ||
-          selectionRange.end.column !== self.lastExecutedSelectionRange.end.column)
-      ) {
-        // Manual execute and there is a selection that is different from the last execute
-        self.result.cancelBatchExecution();
-      }
-      self.lastExecutedSelectionRange = selectionRange;
-    }
-
-    if (self.isCanceling()) {
-      return;
-    }
-    const now = new Date().getTime();
-    if (now - self.lastExecuted() < 1000 || !self.isReady()) {
-      return; // Prevent fast clicks
-    }
-
-    if (!automaticallyTriggered) {
-      // Do not cancel statements that are parts of a set of steps to execute (e.g. import). Only cancel statements as requested by user
-      if (self.status() === 'running' || self.status() === 'loading') {
-        self.cancel(); // TODO: Wait for cancel to finish
-      } else {
-        self.result.clear();
-      }
-    }
-
-    if (self.type() === 'impala') {
-      self.showExecutionAnalysis(false);
-      huePubSub.publish('editor.clear.execution.analysis');
-    }
-
-    self.status('running');
-    self.statusForButtons('executing');
-
-    if (self.isSqlDialect()) {
-      huePubSub.publish('editor.refresh.statement.locations', self);
-    }
-
-    self.lastExecutedStatements = self.statement();
-
-    if (self.ace()) {
-      huePubSub.publish('ace.set.autoexpand', { autoExpand: false, snippet: self });
-      const selectionRange = self.ace().getSelectionRange();
-      self.lastAceSelectionRowOffset(Math.min(selectionRange.start.row, selectionRange.end.row));
-    }
-
-    self.previousChartOptions = vm._getPreviousChartOptions(self);
-    $(document).trigger('executeStarted', { vm: vm, snippet: self });
-    self.lastExecuted(now);
-    $('.jHueNotify').remove();
-    hueAnalytics.log('notebook', 'execute/' + self.type());
-
-    notebook.forceHistoryInitialHeight(true);
-
-    if (self.result.handle()) {
-      self.close();
-    }
-
-    self.errors([]);
-    huePubSub.publish('editor.clear.highlighted.errors', self.ace());
-    self.result.clear();
-    self.progress(0);
-    self.jobs([]);
-    self.result.logs('');
-    self.result.statement_range({
-      start: {
-        row: 0,
-        column: 0
-      },
-      end: {
-        row: 0,
-        column: 0
-      }
-    });
-    notebook.historyCurrentPage(1);
-
-    // TODO: rename startLongOperationTimeout to startBlockingOperationTimeout
-    // TODO: stop blocking operation UI if there is one
-    // TODO: offer to stop blocking submit or fetch operation UI if there is one (add a new call to function for cancelBlockingOperation)
-    // TODO: stop current blocking operation if there is one
-    // TODO: handle jquery.dataTables.1.8.2.min.js:150 Uncaught TypeError: Cannot read property 'asSorting' of undefined on some cancels
-    // TODO: we should cancel blocking operation when leaving notebook (similar to unload())
-    // TODO: we should test when we go back to a query history of a blocking operation that we left
-    startLongOperationTimeout();
-
-    self.currentQueryTab('queryHistory');
-
-    self.executingBlockingOperation = $.post(
-      '/notebook/api/execute/' + self.type(),
-      {
-        notebook: vm.editorMode()
-          ? komapping.toJSON(notebook, NOTEBOOK_MAPPING)
-          : komapping.toJSON(notebook.getContext()),
-        snippet: komapping.toJSON(self.getContext())
-      },
-      data => {
-        try {
-          if (self.isSqlDialect() && data && data.handle) {
-            self.lastExecutedStatement(sqlStatementsParser.parse(data.handle.statement)[0]);
-          } else {
-            self.lastExecutedStatement(null);
-          }
-        } catch (e) {
-          self.lastExecutedStatement(null);
-        }
-        self.statusForButtons('executed');
-        huePubSub.publish('ace.set.autoexpand', { autoExpand: true, snippet: self });
-        stopLongOperationTimeout();
-
-        if (vm.editorMode() && data.history_id) {
-          if (!vm.isNotificationManager()) {
-            const url = vm.URLS.editor + '?editor=' + data.history_id;
-            vm.changeURL(url);
-          }
-          notebook.id(data.history_id);
-          notebook.uuid(data.history_uuid);
-          notebook.isHistory(true);
-          notebook.parentSavedQueryUuid(data.history_parent_uuid);
-        }
-
-        if (data.status === 0) {
-          self.result.handle(data.handle);
-          self.result.hasResultset(data.handle.has_result_set);
-          if (data.handle.sync) {
-            self.loadData(data.result, 100);
-            self.status('available');
-            self.progress(100);
-            self.result.endTime(new Date());
-          } else if (!notebook.unloaded()) {
-            self.checkStatus();
-          }
-          if (vm.isOptimizerEnabled()) {
-            huePubSub.publish('editor.upload.query', data.history_id);
-          }
-        } else {
-          self._ajaxError(data, self.execute);
-          notebook.isExecutingAll(false);
-        }
-
-        if (data.handle) {
-          if (vm.editorMode()) {
-            if (vm.isNotificationManager()) {
-              // Update task status
-              const tasks = $.grep(notebook.history(), row => {
-                return row.uuid() == notebook.uuid();
-              });
-              if (tasks.length === 1) {
-                tasks[0].status(self.status());
-                self.result.logs(data.message);
-              }
-            } else {
-              notebook.history.unshift(
-                notebook._makeHistoryRecord(
-                  undefined,
-                  data.handle.statement,
-                  self.lastExecuted(),
-                  self.status(),
-                  notebook.name(),
-                  notebook.uuid()
-                )
-              );
-            }
-          }
-
-          if (data.handle.statements_count != null) {
-            self.result.statements_count(data.handle.statements_count);
-            self.result.statement_id(data.handle.statement_id);
-            self.result.previous_statement_hash(data.previous_statement_hash);
-
-            if (
-              data.handle.statements_count > 1 &&
-              data.handle.start != null &&
-              data.handle.end != null
-            ) {
-              self.result.statement_range({
-                start: data.handle.start,
-                end: data.handle.end
-              });
-            }
-          }
-        }
-      }
-    )
-      .fail((xhr, textStatus, errorThrown) => {
-        if (self.statusForButtons() != 'canceled' && xhr.status !== 502) {
-          // No error when manually canceled
-          $(document).trigger('error', xhr.responseText);
-        }
-        self.status('failed');
-        self.statusForButtons('executed');
-      })
-      .always(() => {
-        self.executingBlockingOperation = null;
-      });
-  };
-
-  self.reexecute = function() {
-    self.result.cancelBatchExecution();
-    self.execute();
-  };
-
-  self.formatEnabled = ko.pureComputed(() => {
-    return (
-      self.statement_raw && self.statement_raw() != null && self.statement_raw().length < 400000
-    ); // ie: 5000 lines at 80 chars per line
-  });
-
-  self.format = function() {
-    if (self.isSqlDialect()) {
-      apiHelper
-        .formatSql({
-          statements:
-            self.ace().getSelectedText() != '' ? self.ace().getSelectedText() : self.statement_raw()
-        })
-        .done(data => {
-          if (data.status == 0) {
-            if (self.ace().getSelectedText() != '') {
-              self
-                .ace()
-                .session.replace(
-                  self.ace().session.selection.getRange(),
-                  data.formatted_statements
-                );
-            } else {
-              self.statement_raw(data.formatted_statements);
-              self.ace().setValue(self.statement_raw(), 1);
-            }
-          } else {
-            self._ajaxError(data);
-          }
-        });
-    }
-    hueAnalytics.log('notebook', 'format');
-  };
-
-  self.clear = function() {
-    hueAnalytics.log('notebook', 'clear');
-    self.ace().setValue('', 1);
-    self.result.clear();
-    self.status('ready');
-  };
-
-  self.explain = function() {
-    hueAnalytics.log('notebook', 'explain');
-
-    if (self.statement() == '' || self.status() == 'running' || self.status() === 'loading') {
-      return;
-    }
-
-    self.result.explanation('');
-    self.errors([]);
-    self.progress(0);
-    self.status('ready');
-
-    $.post(
-      '/notebook/api/explain',
-      {
-        notebook: komapping.toJSON(notebook.getContext()),
-        snippet: komapping.toJSON(self.getContext())
-      },
-      data => {
-        if (data.status == 0) {
-          self.currentQueryTab('queryExplain');
-          self.result.fetchedOnce(true);
-          self.result.explanation(data.explanation);
-        } else {
-          self._ajaxError(data);
-        }
-      }
-    );
-  };
-
-  let lastCompatibilityRequest;
-
-  self.checkCompatibility = function() {
-    self.hasSuggestion(null);
-    self.compatibilitySourcePlatform(COMPATIBILITY_SOURCE_PLATFORMS[self.type()]);
-    self.compatibilityTargetPlatform(
-      COMPATIBILITY_TARGET_PLATFORMS[self.type() === 'hive' ? 'impala' : 'hive']
-    );
-    self.queryCompatibility();
-  };
-
-  self.queryCompatibility = function(targetPlatform) {
-    apiHelper.cancelActiveRequest(lastCompatibilityRequest);
-
-    hueAnalytics.log('notebook', 'compatibility');
-    self.compatibilityCheckRunning(targetPlatform != self.type());
-    self.hasSuggestion(null);
-    const positionStatement = self.positionStatement();
-
-    lastCompatibilityRequest = $.post(
-      '/notebook/api/optimizer/statement/compatibility',
-      {
-        notebook: komapping.toJSON(notebook.getContext()),
-        snippet: komapping.toJSON(self.getContext()),
-        sourcePlatform: self.compatibilitySourcePlatform().value,
-        targetPlatform: self.compatibilityTargetPlatform().value
-      },
-      data => {
-        if (data.status === 0) {
-          self.aceErrorsHolder([]);
-          self.aceWarningsHolder([]);
-          self.suggestion(komapping.fromJS(data.query_compatibility));
-          if (self.suggestion().queryError && self.suggestion().queryError.errorString()) {
-            const match = ERROR_REGEX.exec(self.suggestion().queryError.errorString());
-            let line = null;
-            if (match) {
-              if (positionStatement) {
-                line = positionStatement.location.first_line + parseInt(match[1]) + 1;
-              } else {
-                line = parseInt(match[1]) - 1;
-              }
-            }
-            self.aceWarningsHolder.push({
-              message: self.suggestion().queryError.errorString(),
-              line: line,
-              col:
-                match === null ? null : typeof match[3] !== 'undefined' ? parseInt(match[3]) : null
-            });
-            self.status('with-optimizer-report');
-          }
-          if (self.suggestion().parseError()) {
-            const match = ERROR_REGEX.exec(self.suggestion().parseError());
-            self.aceErrorsHolder.push({
-              message: self.suggestion().parseError(),
-              line: match === null ? null : parseInt(match[1]) - 1,
-              col:
-                match === null ? null : typeof match[3] !== 'undefined' ? parseInt(match[3]) : null
-            });
-            self.status('with-optimizer-report');
-          }
-          self.showOptimizer(true);
-          self.hasSuggestion(true);
-        } else {
-          $(document).trigger('error', data.message);
-        }
-      }
-    )
-      .fail((xhr, textStatus, errorThrown) => {
-        if (xhr.status !== 502) {
-          $(document).trigger('error', xhr.responseText);
-        }
-      })
-      .always(() => {
-        self.compatibilityCheckRunning(false);
-      });
-  };
-
-  self.fetchResult = function(rows, startOver) {
-    if (typeof startOver == 'undefined') {
-      startOver = true;
-    }
-    self.fetchResultData(rows, startOver);
-    //self.fetchResultMetadata(rows);
-  };
-
-  self.isFetchingData = false;
-
-  self.fetchExecutionAnalysis = function() {
-    if (self.type() === 'impala') {
-      // TODO: Use real query ID
-      huePubSub.publish('editor.update.execution.analysis', {
-        analysisPossible: true,
-        compute: self.compute(),
-        queryId: notebook.getContext().id(),
-        name: self.jobs()[0] && self.jobs()[0].name
-      });
-    } else {
-      huePubSub.publish('editor.update.execution.analysis', {
-        analysisPossible: false
-      });
-    }
-  };
-
-  self.fetchResultData = function(rows, startOver) {
-    if (!self.isFetchingData) {
-      if (self.status() === 'available') {
-        startLongOperationTimeout();
-        self.isFetchingData = true;
-        hueAnalytics.log('notebook', 'fetchResult/' + rows + '/' + startOver);
-        $.post(
-          '/notebook/api/fetch_result_data',
-          {
-            notebook: komapping.toJSON(notebook.getContext()),
-            snippet: komapping.toJSON(self.getContext()),
-            rows: rows,
-            startOver: startOver
-          },
-          data => {
-            stopLongOperationTimeout();
-            data = JSON.bigdataParse(data);
-            if (data.status === 0) {
-              self.showExecutionAnalysis(true);
-              self.loadData(data.result, rows);
-            } else {
-              self._ajaxError(data, () => {
-                self.isFetchingData = false;
-                self.fetchResultData(rows, startOver);
-              });
-              $(document).trigger('renderDataError', { snippet: self });
-            }
-          },
-          'text'
-        )
-          .fail((xhr, textStatus, errorThrown) => {
-            if (xhr.status !== 502) {
-              $(document).trigger('error', xhr.responseText);
-            }
-          })
-          .always(() => {
-            self.isFetchingData = false;
-          });
-      } else {
-        huePubSub.publish('editor.snippet.result.normal', self);
-      }
-    }
-  };
-
-  self.loadData = function(result, rows) {
-    rows -= result.data.length;
-
-    if (result.data.length > 0) {
-      self.currentQueryTab('queryResults');
-    }
-
-    const _initialIndex = self.result.data().length;
-    const _tempData = [];
-    $.each(result.data, (index, row) => {
-      row.unshift(_initialIndex + index + 1);
-      self.result.data.push(row);
-      _tempData.push(row);
-    });
-
-    if (self.result.rows() == null || (self.result.rows() + '').indexOf('+') != -1) {
-      self.result.rows(self.result.data().length + (result.has_more ? '+' : ''));
-    }
-
-    self.result.images(
-      typeof result.images != 'undefined' && result.images != null ? result.images : []
-    );
-
-    huePubSub.publish('editor.render.data', {
-      data: _tempData,
-      snippet: self,
-      initial: _initialIndex == 0
-    });
-
-    if (!self.result.fetchedOnce()) {
-      result.meta.unshift({ type: 'INT_TYPE', name: '', comment: null });
-      self.result.meta(result.meta);
-      self.result.type(result.type);
-      self.result.fetchedOnce(true);
-    }
-
-    self.result.meta().forEach(meta => {
-      if (
-        $.inArray(meta.type, [
-          'TINYINT_TYPE',
-          'SMALLINT_TYPE',
-          'INT_TYPE',
-          'BIGINT_TYPE',
-          'FLOAT_TYPE',
-          'DOUBLE_TYPE',
-          'DECIMAL_TYPE'
-        ]) > -1
-      ) {
-        meta.cssClass = 'sort-numeric';
-      } else if ($.inArray(meta.type, ['TIMESTAMP_TYPE', 'DATE_TYPE', 'DATETIME_TYPE']) > -1) {
-        meta.cssClass = 'sort-date';
-      } else {
-        meta.cssClass = 'sort-string';
-      }
-    });
-
-    self.result.hasMore(result.has_more);
-
-    if (result.has_more && rows > 0) {
-      setTimeout(() => {
-        self.fetchResultData(rows, false);
-      }, 500);
-    } else if (
-      !vm.editorMode() &&
-      !notebook.isPresentationMode() &&
-      notebook.snippets()[notebook.snippets().length - 1] == self
-    ) {
-      notebook.newSnippet();
-    }
-  };
-
-  self.fetchResultMetadata = function() {
-    $.post(
-      '/notebook/api/fetch_result_metadata',
-      {
-        notebook: komapping.toJSON(notebook.getContext()),
-        snippet: komapping.toJSON(self.getContext())
-      },
-      data => {
-        if (data.status == 0) {
-          self.result.meta(data.result.meta);
-        } else {
-          $(document).trigger('error', data.message);
-        }
-      }
-    ).fail((xhr, textStatus, errorThrown) => {
-      if (xhr.status !== 502) {
-        $(document).trigger('error', xhr.responseText);
-      }
-      self.status('failed');
-    });
-  };
-
-  self.fetchResultSize = function(n, query_id) {
-    $.post(
-      '/notebook/api/fetch_result_size',
-      {
-        notebook: komapping.toJSON(notebook.getContext()),
-        snippet: komapping.toJSON(self.getContext())
-      },
-      data => {
-        if (query_id == notebook.id()) {
-          // If still on the same result
-          if (data.status == 0) {
-            if (data.result.rows != null) {
-              self.result.rows(data.result.rows);
-            } else if (self.type() == 'impala' && n > 0) {
-              setTimeout(() => {
-                self.fetchResultSize(n - 1, query_id);
-              }, 1000);
-            }
-          } else if (data.status == 5) {
-            // No supported yet for this snippet
-          } else {
-            //$(document).trigger("error", data.message);
-          }
-        }
-      }
-    ).fail((xhr, textStatus, errorThrown) => {
-      //$(document).trigger("error", xhr.responseText);
-    });
-  };
-
-  self.checkStatus = function() {
-    $.post(
-      '/notebook/api/check_status',
-      {
-        notebook: komapping.toJSON(notebook.getContext()),
-        snippet: komapping.toJSON(self.getContext())
-      },
-      data => {
-        if (self.statusForButtons() == 'canceling' || self.status() == 'canceled') {
-          // Query was canceled in the meantime, do nothing
-        } else {
-          self.result.endTime(new Date());
-
-          if (data.status === 0) {
-            self.status(data.query_status.status);
-
-            if (
-              self.status() == 'running' ||
-              self.status() == 'starting' ||
-              self.status() == 'waiting'
-            ) {
-              const delay = self.result.executionTime() > 45000 ? 5000 : 1000; // 5s if more than 45s
-              if (!notebook.unloaded()) {
-                self.checkStatusTimeout = setTimeout(self.checkStatus, delay);
-              }
-            } else if (self.status() === 'available') {
-              self.fetchResult(100);
-              self.progress(100);
-              if (self.isSqlDialect()) {
-                if (self.result.handle().has_result_set) {
-                  const _query_id = notebook.id();
-                  setTimeout(() => {
-                    // Delay until we get IMPALA-5555
-                    self.fetchResultSize(10, _query_id);
-                  }, 2000);
-                  self.checkDdlNotification(); // DDL CTAS with Impala
-                } else if (self.lastExecutedStatement()) {
-                  self.checkDdlNotification();
-                } else {
-                  self.onDdlExecute();
-                }
-              }
-              if (notebook.isExecutingAll()) {
-                notebook.executingAllIndex(notebook.executingAllIndex() + 1);
-                if (notebook.executingAllIndex() < notebook.snippets().length) {
-                  notebook.snippets()[notebook.executingAllIndex()].execute();
-                } else {
-                  notebook.isExecutingAll(false);
-                }
-              }
-              if (!self.result.handle().has_more_statements && vm.successUrl()) {
-                window.location.href = vm.successUrl(); // Not used anymore in Hue 4
-              }
-            } else if (self.status() === 'success') {
-              self.progress(99);
-            }
-          } else if (data.status === -3) {
-            self.status('expired');
-            notebook.isExecutingAll(false);
-          } else {
-            self._ajaxError(data);
-            notebook.isExecutingAll(false);
-          }
-          self.getLogs(); // Need to execute at the end, because updating the status impacts log progress results
-        }
-      }
-    ).fail((xhr, textStatus, errorThrown) => {
-      if (xhr.status !== 502) {
-        $(document).trigger('error', xhr.responseText || textStatus);
-      }
-      self.status('failed');
-      notebook.isExecutingAll(false);
-    });
-  };
-
-  self.checkDdlNotification = function() {
-    if (
-      self.lastExecutedStatement() &&
-      /ALTER|CREATE|DELETE|DROP|GRANT|INSERT|LOAD|SET|TRUNCATE|UPDATE|UPSERT|USE/i.test(
-        self.lastExecutedStatement().firstToken
-      )
-    ) {
-      self.onDdlExecute();
-    } else {
-      window.clearTimeout(self.executeNextTimeout);
-    }
-  };
-
-  self.isCanceling = ko.observable(false);
-
-  self.cancel = function() {
-    window.clearTimeout(self.executeNextTimeout);
-    self.isCanceling(true);
-    if (self.checkStatusTimeout != null) {
-      clearTimeout(self.checkStatusTimeout);
-      self.checkStatusTimeout = null;
-    }
-    hueAnalytics.log('notebook', 'cancel');
-
-    if (self.executingBlockingOperation != null) {
-      self.executingBlockingOperation.abort();
-      self.executingBlockingOperation = null;
-    }
-
-    if ($.isEmptyObject(self.result.handle())) {
-      // Query was not even submitted yet
-      self.statusForButtons('canceled');
-      self.status('failed');
-      self.isCanceling(false);
-      notebook.isExecutingAll(false);
-    } else {
-      self.statusForButtons('canceling');
-      $.post(
-        '/notebook/api/cancel_statement',
-        {
-          notebook: komapping.toJSON(notebook.getContext()),
-          snippet: komapping.toJSON(self.getContext())
-        },
-        data => {
-          self.statusForButtons('canceled');
-          if (data.status == 0) {
-            self.status('canceled');
-            notebook.isExecutingAll(false);
-          } else {
-            self._ajaxError(data);
-          }
-        }
-      )
-        .fail((xhr, textStatus, errorThrown) => {
-          if (xhr.status !== 502) {
-            $(document).trigger('error', xhr.responseText);
-          }
-          self.statusForButtons('canceled');
-          self.status('failed');
-          notebook.isExecutingAll(false);
-        })
-        .always(() => {
-          self.isCanceling(false);
-        });
-    }
-  };
-
-  self.close = function() {
-    if (self.checkStatusTimeout != null) {
-      clearTimeout(self.checkStatusTimeout);
-      self.checkStatusTimeout = null;
-    }
-
-    $.post(
-      '/notebook/api/close_statement',
-      {
-        notebook: komapping.toJSON(notebook.getContext()),
-        snippet: komapping.toJSON(self.getContext())
-      },
-      data => {
-        if (data.status == 0) {
-          // self.status('closed'); // Keep as 'running' as currently it happens before running a new query
-        } else {
-          // self._ajaxError(data);
-        }
-      }
-    ).fail((xhr, textStatus, errorThrown) => {
-      if (xhr.status !== 502) {
-        // $(document).trigger("error", xhr.responseText);
-      }
-      // self.status('failed'); // Can conflict with slow close and new query execution
-    });
-  };
-
-  self.getLogs = function() {
-    $.post(
-      '/notebook/api/get_logs',
-      {
-        notebook: komapping.toJSON(notebook.getContext()),
-        snippet: komapping.toJSON(self.getContext()),
-        from: self.result.logLines,
-        jobs: komapping.toJSON(self.jobs, { ignore: ['percentJob'] }),
-        full_log: self.result.logs
-      },
-      data => {
-        if (data.status == 1) {
-          // Append errors to the logs
-          data.status = 0;
-          data.logs = data.message;
-        }
-        if (data.status == 0) {
-          if (data.logs.length > 0) {
-            const logs = data.logs.split('\n');
-            self.result.logLines += logs.length;
-            const oldLogs = self.result.logs();
-            if (
-              data.logs &&
-              (oldLogs === '' ||
-                (self.wasBatchExecuted() && data.logs.indexOf('Unable to locate') == -1) ||
-                data.isFullLogs)
-            ) {
-              self.result.logs(data.logs);
-            } else {
-              self.result.logs(oldLogs + '\n' + data.logs);
-            }
-          }
-
-          self.jobs().forEach(job => {
-            if (typeof job.percentJob === 'undefined') {
-              job.percentJob = ko.observable(-1);
-            }
-          });
-
-          if (data.jobs && data.jobs.length > 0) {
-            data.jobs.forEach(job => {
-              const _found = ko.utils.arrayFilter(self.jobs(), item => {
-                return item.name === job.name;
-              });
-              if (_found.length === 0) {
-                if (typeof job.percentJob === 'undefined') {
-                  job.percentJob = ko.observable(-1);
-                } else {
-                  job.percentJob = ko.observable(job.percentJob);
-                }
-                self.jobs.push(job);
-              } else if (typeof job.percentJob !== 'undefined') {
-                for (let i = 0; i < _found.length; i++) {
-                  _found[i].percentJob(job.percentJob);
-                }
-              }
-            });
-            self.jobs().forEach(job => {
-              const _found = ko.utils.arrayFilter(self.jobs(), item => {
-                return item.name === job.name;
-              });
-              if (_found.length === 0) {
-                self.jobs.remove(job);
-              }
-            });
-          }
-          if (self.status() == 'running') {
-            // Maybe the query finished or failed in the meantime
-            self.progress(data.progress);
-          }
-        } else {
-          self._ajaxError(data);
-        }
-      }
-    ).fail((xhr, textStatus, errorThrown) => {
-      if (xhr.status !== 502) {
-        $(document).trigger('error', xhr.responseText || textStatus);
-      }
-      self.status('failed');
-    });
-  };
-
-  self.uploadQueryHistory = function(n) {
-    hueAnalytics.log('notebook', 'upload_query_history');
-
-    $.post(
-      '/metadata/api/optimizer/upload/history',
-      {
-        n: typeof n != 'undefined' ? n : null,
-        sourcePlatform: self.type()
-      },
-      data => {
-        if (data.status == 0) {
-          $(document).trigger(
-            'info',
-            data.upload_history[self.type()].count +
-              ' queries uploaded successfully. Processing them...'
-          );
-          self.watchUploadStatus(data.upload_history[self.type()].status.workloadId);
-        } else {
-          $(document).trigger('error', data.message);
-        }
-      }
-    );
-  };
-
-  self.uploadQuery = function(query_id) {
-    $.post('/metadata/api/optimizer/upload/query', {
-      query_id: query_id,
-      sourcePlatform: self.type()
-    });
-  };
-
-  self.uploadTableStats = function(options) {
-    hueAnalytics.log('notebook', 'load_table_stats');
-    if (options.showProgress) {
-      $(document).trigger('info', 'Preparing table data...');
-    }
-
-    $.post(
-      '/metadata/api/optimizer/upload/table_stats',
-      {
-        db_tables: komapping.toJSON(
-          $.map(options.activeTables, table => {
-            return table.databaseName + '.' + table.tableName;
-          })
-        ),
-        sourcePlatform: komapping.toJSON(self.type()),
-        with_ddl: komapping.toJSON(true),
-        with_table_stats: komapping.toJSON(true),
-        with_columns_stats: komapping.toJSON(true)
-      },
-      data => {
-        if (data.status == 0) {
-          if (options.showProgress) {
-            $(document).trigger(
-              'info',
-              $.map(options.activeTables, table => {
-                return table.tableName;
-              }) + ' stats sent to analyse'
-            );
-          }
-          if (data.upload_table_ddl && options.showProgress) {
-            // With showProgress only currently as can be very slow
-            self.watchUploadStatus(data.upload_table_ddl.status.workloadId, options.showProgress);
-          }
-        } else if (options.showProgress) {
-          $(document).trigger('error', data.message);
-        }
-      }
-    ).always(() => {
-      if (options.callback) {
-        options.callback();
-      }
-    });
-  };
-
-  self.watchUploadStatus = function(workloadId, showProgress) {
-    $.post(
-      '/metadata/api/optimizer/upload/status',
-      {
-        workloadId: workloadId
-      },
-      data => {
-        if (data.status == 0) {
-          if (showProgress) {
-            $(document).trigger('info', 'Query processing: ' + data.upload_status.status.state);
-          }
-          if (['WAITING', 'IN_PROGRESS'].indexOf(data.upload_status.status.state) != -1) {
-            window.setTimeout(() => {
-              self.watchUploadStatus(workloadId);
-            }, 2000);
-          } else if (showProgress) {
-            $(document).trigger(
-              'warn',
-              data.upload_status.status.statusMsg +
-                (data.upload_status.status.failedQueries > 0
-                  ? '. ' +
-                    data.upload_status.status.failQueryDetails.map(query => {
-                      return query.error;
-                    })
-                  : '')
-            );
-          }
-        } else if (showProgress) {
-          $(document).trigger('error', data.message);
-        }
-      }
-    );
-  };
-
-  self.getSimilarQueries = function() {
-    hueAnalytics.log('notebook', 'get_query_similarity');
-
-    $.post(
-      '/notebook/api/optimizer/statement/similarity',
-      {
-        notebook: komapping.toJSON(notebook.getContext()),
-        snippet: komapping.toJSON(self.getContext()),
-        sourcePlatform: self.type()
-      },
-      data => {
-        if (data.status == 0) {
-          console.log(data.statement_similarity);
-        } else {
-          $(document).trigger('error', data.message);
-        }
-      }
-    );
-  };
-
-  self.autocompleter = new AceAutocompleteWrapper({
-    snippet: self,
-    user: vm.user,
-    optEnabled: false,
-    timeout: vm.autocompleteTimeout
-  });
-
-  self.init = function() {
-    if ((self.status() == 'running' || self.status() == 'available') && notebook.isHistory()) {
-      self.checkStatus();
-    } else if (self.status() == 'loading') {
-      self.status('failed');
-      self.progress(0);
-      self.jobs([]);
-    } else if (self.status() == 'ready-execute') {
-      self.execute();
-    }
-  };
-
-  self.onKeydownInVariable = function(context, e) {
-    if ((e.ctrlKey || e.metaKey) && e.which === 13) {
-      // Ctrl-enter
-      self.ace().commands.commands['execute'].exec();
-    } else if ((e.ctrlKey || e.metaKey) && e.which === 83) {
-      // Ctrl-s
-      self.ace().commands.commands['save'].exec();
-      e.preventDefault(); // Prevent browser page save dialog
-    }
-    return true;
-  };
-};
-
-const Session = function(vm, session) {
-  const self = this;
-  komapping.fromJS(session, {}, self);
-
-  self.selectedSessionProperty = ko.observable('');
-
-  self.restarting = ko.observable(false);
-
-  if (!ko.isObservable(self.properties)) {
-    self.properties = ko.observableArray();
-  }
-
-  self.availableNewProperties = ko.computed(() => {
-    const addedIndex = {};
-    $.each(self.properties(), (index, property) => {
-      addedIndex[property.key] = true;
-    });
-    const result = $.grep(vm.availableSessionProperties(), property => {
-      return !addedIndex[property.name];
-    });
-    return result;
-  });
-};
-
-const Notebook = function(vm, notebook) {
-  const self = this;
-
-  self.id = ko.observable(
-    typeof notebook.id != 'undefined' && notebook.id != null ? notebook.id : null
-  );
-  self.uuid = ko.observable(
-    typeof notebook.uuid != 'undefined' && notebook.uuid != null ? notebook.uuid : hueUtils.UUID()
-  );
-  self.name = ko.observable(
-    typeof notebook.name != 'undefined' && notebook.name != null ? notebook.name : 'My Notebook'
-  );
-  self.description = ko.observable(
-    typeof notebook.description != 'undefined' && notebook.description != null
-      ? notebook.description
-      : ''
-  );
-  self.type = ko.observable(
-    typeof notebook.type != 'undefined' && notebook.type != null ? notebook.type : 'notebook'
-  );
-  self.initialType = self.type().replace('query-', '');
-  self.coordinatorUuid = ko.observable(
-    typeof notebook.coordinatorUuid != 'undefined' && notebook.coordinatorUuid != null
-      ? notebook.coordinatorUuid
-      : null
-  );
-  self.isHistory = ko.observable(
-    typeof notebook.is_history != 'undefined' && notebook.is_history != null
-      ? notebook.is_history
-      : false
-  );
-  self.isManaged = ko.observable(
-    typeof notebook.isManaged != 'undefined' && notebook.isManaged != null
-      ? notebook.isManaged
-      : false
-  );
-  self.parentSavedQueryUuid = ko.observable(
-    typeof notebook.parentSavedQueryUuid != 'undefined' && notebook.parentSavedQueryUuid != null
-      ? notebook.parentSavedQueryUuid
-      : null
-  ); // History parent
-  self.isSaved = ko.observable(
-    typeof notebook.isSaved != 'undefined' && notebook.isSaved != null ? notebook.isSaved : false
-  );
-  self.canWrite = ko.observable(
-    typeof notebook.can_write != 'undefined' && notebook.can_write != null
-      ? notebook.can_write
-      : true
-  );
-  self.onSuccessUrl = ko.observable(
-    typeof notebook.onSuccessUrl != 'undefined' && notebook.onSuccessUrl != null
-      ? notebook.onSuccessUrl
-      : null
-  );
-  self.pubSubUrl = ko.observable(
-    typeof notebook.pubSubUrl != 'undefined' && notebook.pubSubUrl != null
-      ? notebook.pubSubUrl
-      : null
-  );
-  self.isPresentationModeDefault = ko.observable(
-    typeof notebook.isPresentationModeDefault != 'undefined' &&
-      notebook.isPresentationModeDefault != null
-      ? notebook.isPresentationModeDefault
-      : false
-  );
-  self.isPresentationMode = ko.observable(false);
-  self.isPresentationModeInitialized = ko.observable(false);
-  self.isPresentationMode.subscribe(newValue => {
-    if (!newValue) {
-      self.cancelExecutingAll();
-    }
-    huePubSub.publish('editor.presentation.operate.toggle', newValue); // Problem with headers / row numbers redraw on full screen results
-    vm.togglePresentationMode();
-    if (newValue) {
-      hueAnalytics.convert('editor', 'presentation');
-    }
-  });
-  self.presentationSnippets = ko.observable({});
-  self.isHidingCode = ko.observable(
-    typeof notebook.isHidingCode != 'undefined' && notebook.isHidingCode != null
-      ? notebook.isHidingCode
-      : false
-  );
-
-  self.snippets = ko.observableArray();
-  self.selectedSnippet = ko.observable(vm.editorType()); // Aka selectedSnippetType
-  self.creatingSessionLocks = ko.observableArray();
-  self.sessions = komapping.fromJS(
-    typeof notebook.sessions != 'undefined' && notebook.sessions != null ? notebook.sessions : [],
-    {
-      create: function(value) {
-        return new Session(vm, value.data);
-      }
-    }
-  );
-  self.directoryUuid = ko.observable(
-    typeof notebook.directoryUuid != 'undefined' && notebook.directoryUuid != null
-      ? notebook.directoryUuid
-      : null
-  );
-  self.dependents = komapping.fromJS(
-    typeof notebook.dependents != 'undefined' && notebook.dependents != null
-      ? notebook.dependents
-      : []
-  );
-  self.dependentsCoordinator = ko.computed(() => {
-    return $.grep(self.dependents(), doc => {
-      return doc.type() == 'oozie-coordinator2' && doc.is_managed() == true;
-    });
-  });
-  if (self.dependentsCoordinator().length > 0 && !self.coordinatorUuid()) {
-    self.coordinatorUuid(self.dependentsCoordinator()[0].uuid());
-  }
-  self.history = ko.observableArray(
-    vm.selectedNotebook() &&
-      vm.selectedNotebook().history().length > 0 &&
-      vm.selectedNotebook().history()[0].type == self.type()
-      ? vm.selectedNotebook().history()
-      : []
-  );
-  self.history.subscribe(val => {
-    if (
-      self.id() == null &&
-      val.length == 0 &&
-      self.historyFilter() === '' &&
-      !vm.isNotificationManager()
-    ) {
-      self
-        .snippets()[0]
-        .currentQueryTab(
-          typeof IS_EMBEDDED !== 'undefined' && IS_EMBEDDED ? 'queryHistory' : 'savedQueries'
-        );
-    }
-  });
-  self.historyFilter = ko.observable('');
-  self.historyFilterVisible = ko.observable(false);
-  self.historyFilter.extend({ rateLimit: { method: 'notifyWhenChangesStop', timeout: 900 } });
-  self.historyFilter.subscribe(val => {
-    if (self.historyCurrentPage() != 1) {
-      self.historyCurrentPage(1);
-    } else {
-      self.fetchHistory();
-    }
-  });
-  self.loadingHistory = ko.observable(self.history().length == 0);
-  self.historyInitialHeight = ko.observable(0).extend({ throttle: 1000 });
-  self.forceHistoryInitialHeight = ko.observable(false);
-  self.historyCurrentPage = ko.observable(
-    vm.selectedNotebook() ? vm.selectedNotebook().historyCurrentPage() : 1
-  );
-  self.historyCurrentPage.subscribe(val => {
-    self.fetchHistory();
-  });
-  self.historyTotalPages = ko.observable(
-    vm.selectedNotebook() ? vm.selectedNotebook().historyTotalPages() : 1
-  );
-
-  self.schedulerViewModel = null;
-  self.schedulerViewModelIsLoaded = ko.observable(false);
-  self.schedulerViewerViewModel = ko.observable();
-  self.isBatchable = ko.computed(() => {
-    return (
-      self.snippets().length > 0 &&
-      $.grep(self.snippets(), snippet => {
-        return snippet.isBatchable();
-      }).length == self.snippets().length
-    );
-  });
-
-  self.isExecutingAll = ko.observable(!!notebook.isExecutingAll);
-  self.cancelExecutingAll = function() {
-    const index = self.executingAllIndex();
-    if (self.isExecutingAll() && self.snippets()[index]) {
-      self.snippets()[index].cancel();
-    }
-  };
-  self.executingAllIndex = ko.observable(notebook.executingAllIndex || 0);
-
-  self.retryModalConfirm = null;
-  self.retryModalCancel = null;
-
-  self.avoidClosing = false;
-
-  self.canSave = vm.canSave;
-
-  self.getSession = function(session_type) {
-    let _s = null;
-    $.each(self.sessions(), (index, s) => {
-      if (s.type() == session_type) {
-        _s = s;
-        return false;
-      }
-    });
-    return _s;
-  };
-
-  self.getSnippets = function(type) {
-    return $.grep(self.snippets(), snippet => {
-      return snippet.type() == type;
-    });
-  };
-
-  self.unloaded = ko.observable(false);
-  self.unload = function() {
-    self.unloaded(true);
-    let currentQueries = null;
-    self.snippets().forEach(snippet => {
-      if (snippet.checkStatusTimeout != null) {
-        clearTimeout(snippet.checkStatusTimeout);
-        snippet.checkStatusTimeout = null;
-      }
-      if (currentQueries == null) {
-        currentQueries = snippet.queries();
-      }
-    });
-    return currentQueries;
-  };
-
-  self.restartSession = function(session, callback) {
-    if (session.restarting()) {
-      return;
-    }
-    session.restarting(true);
-    const snippets = self.getSnippets(session.type());
-
-    $.each(snippets, (index, snippet) => {
-      snippet.status('loading');
-    });
-
-    self.closeSession(session, true, () => {
-      self.createSession(
-        session,
-        () => {
-          $.each(snippets, (index, snippet) => {
-            snippet.status('ready');
-          });
-          session.restarting(false);
-          if (callback) {
-            callback();
-          }
-        },
-        () => {
-          session.restarting(false);
-        }
-      );
-    });
-  };
-
-  self.addSession = function(session) {
-    const toRemove = [];
-    $.each(self.sessions(), (index, s) => {
-      if (s.type() == session.type()) {
-        toRemove.push(s);
-      }
-    });
-
-    $.each(toRemove, (index, s) => {
-      self.sessions.remove(s);
-    });
-
-    self.sessions.push(session);
-  };
-
-  self.addSnippet = function(snippet, skipSession) {
-    const _snippet = new Snippet(vm, self, snippet);
-    self.snippets.push(_snippet);
-
-    if (self.getSession(_snippet.type()) == null && typeof skipSession == 'undefined') {
-      window.setTimeout(() => {
-        _snippet.status('loading');
-        self.createSession(new Session(vm, { type: _snippet.type() }));
-      }, 200);
-    }
-
-    _snippet.init();
-    return _snippet;
-  };
-
-  self.createSession = function(session, callback, failCallback) {
-    if (self.creatingSessionLocks().indexOf(session.type()) != -1) {
-      // Create one type of session max
-      return;
-    } else {
-      self.creatingSessionLocks.push(session.type());
-    }
-
-    let compute = null;
-    $.each(self.getSnippets(session.type()), (index, snippet) => {
-      snippet.status('loading');
-      if (index == 0) {
-        compute = snippet.compute();
-      }
-    });
-
-    const fail = function(message) {
-      $.each(self.getSnippets(session.type()), (index, snippet) => {
-        snippet.status('failed');
-      });
-      $(document).trigger('error', message);
-      if (failCallback) {
-        failCallback();
-      }
-    };
-
-    $.post(
-      '/notebook/api/create_session',
-      {
-        notebook: komapping.toJSON(self.getContext()),
-        session: komapping.toJSON(session), // e.g. {'type': 'pyspark', 'properties': [{'name': driverCores', 'value', '2'}]}
-        cluster: komapping.toJSON(compute ? compute : '')
-      },
-      data => {
-        if (data.status == 0) {
-          komapping.fromJS(data.session, {}, session);
-          if (self.getSession(session.type()) == null) {
-            self.addSession(session);
-          }
-          $.each(self.getSnippets(session.type()), (index, snippet) => {
-            snippet.status('ready');
-          });
-          if (callback) {
-            setTimeout(callback, 500);
-          }
-        } else if (data.status == 401) {
-          $(document).trigger('showAuthModal', { type: session.type() });
-        } else {
-          fail(data.message);
-        }
-      }
-    )
-      .fail(xhr => {
-        if (xhr.status !== 502) {
-          fail(xhr.responseText);
-        }
-      })
-      .always(() => {
-        self.creatingSessionLocks.remove(session.type());
-      });
-  };
-
-  self.authSession = function() {
-    self.createSession(
-      new Session(vm, {
-        type: vm.authSessionType(),
-        properties: [
-          { name: 'user', value: vm.authSessionUsername() },
-          { name: 'password', value: vm.authSessionPassword() }
-        ]
-      }),
-      vm.authSessionCallback() // On new session we don't automatically execute the snippet after the aut. On session expiration we do or we refresh assist DB when login-in.
-    );
-  };
-
-  self.newSnippet = function(type) {
-    if (type) {
-      self.selectedSnippet(type);
-    }
-    const snippet = self.addSnippet({
-      type: self.selectedSnippet(),
-      result: {}
-    });
-
-    window.setTimeout(() => {
-      const lastSnippet = snippet;
-      if (lastSnippet.ace() != null) {
-        lastSnippet.ace().focus();
-      }
-    }, 100);
-
-    hueAnalytics.log('notebook', 'add_snippet/' + (type ? type : self.selectedSnippet()));
-    return snippet;
-  };
-
-  self.newSnippetAbove = function(id) {
-    self.newSnippet();
-    let idx = 0;
-    self.snippets().forEach((snippet, cnt) => {
-      if (snippet.id() == id) {
-        idx = cnt;
-      }
-    });
-    self.snippets(self.snippets().move(self.snippets().length - 1, idx));
-  };
-
-  self.getContext = function() {
-    return {
-      id: self.id,
-      uuid: self.uuid,
-      parentSavedQueryUuid: self.parentSavedQueryUuid,
-      isSaved: self.isSaved,
-      sessions: self.sessions,
-      type: self.type,
-      name: self.name
-    };
-  };
-
-  self.save = function(callback) {
-    hueAnalytics.log('notebook', 'save');
-
-    // Remove the result data from the snippets
-    // Also do it for presentation mode
-    const cp = komapping.toJS(self, NOTEBOOK_MAPPING);
-    $.each(
-      cp.snippets.concat(
-        Object.keys(cp.presentationSnippets).map(key => {
-          return cp.presentationSnippets[key];
-        })
-      ),
-      (index, snippet) => {
-        snippet.result.data.length = 0; // snippet.result.clear() does not work for some reason
-        snippet.result.meta.length = 0;
-        snippet.result.logs = '';
-        snippet.result.fetchedOnce = false;
-        snippet.progress = 0; // Remove progress
-        snippet.jobs.length = 0;
-      }
-    );
-    if (cp.schedulerViewModel) {
-      cp.schedulerViewModel.availableTimezones = [];
-    }
-    const editorMode =
-      vm.editorMode() || (self.isPresentationMode() && vm.editorType() != 'notebook'); // Editor should not convert to Notebook in presentation mode
-
-    $.post(
-      '/notebook/api/notebook/save',
-      {
-        notebook: komapping.toJSON(cp, NOTEBOOK_MAPPING),
-        editorMode: editorMode
-      },
-      data => {
-        if (data.status == 0) {
-          self.id(data.id);
-          self.isSaved(true);
-          const wasHistory = self.isHistory();
-          self.isHistory(false);
-          $(document).trigger('info', data.message);
-          if (editorMode) {
-            if (!data.save_as) {
-              const existingQuery = self
-                .snippets()[0]
-                .queries()
-                .filter(item => {
-                  return item.uuid() === data.uuid;
-                });
-              if (existingQuery.length > 0) {
-                existingQuery[0].name(data.name);
-                existingQuery[0].description(data.description);
-                existingQuery[0].last_modified(data.last_modified);
-              }
-            } else if (self.snippets()[0].queries().length > 0) {
-              // Saved queries tab already loaded
-              self.snippets()[0].queries.unshift(komapping.fromJS(data));
-            }
-
-            if (self.coordinatorUuid() && self.schedulerViewModel) {
-              self.saveScheduler();
-              self.schedulerViewModel.coordinator.refreshParameters();
-            }
-            if (wasHistory || data.save_as) {
-              self.loadScheduler();
-            }
-
-            if (
-              self.snippets()[0].downloadResultViewModel &&
-              self
-                .snippets()[0]
-                .downloadResultViewModel()
-                .saveTarget() === 'dashboard'
-            ) {
-              huePubSub.publish(
-                'open.link',
-                vm.URLS.report +
-                  '&uuid=' +
-                  data.uuid +
-                  '&statement=' +
-                  self.snippets()[0].result.handle().statement_id
-              );
-            } else {
-              vm.changeURL(vm.URLS.editor + '?editor=' + data.id);
-            }
-          } else {
-            vm.changeURL(vm.URLS.notebook + '?notebook=' + data.id);
-          }
-          if (typeof callback == 'function') {
-            callback();
-          }
-        } else {
-          $(document).trigger('error', data.message);
-        }
-      }
-    ).fail((xhr, textStatus, errorThrown) => {
-      if (xhr.status !== 502) {
-        $(document).trigger('error', xhr.responseText);
-      }
-    });
-  };
-
-  self.close = function() {
-    hueAnalytics.log('notebook', 'close');
-    $.post('/notebook/api/notebook/close', {
-      notebook: komapping.toJSON(self, NOTEBOOK_MAPPING),
-      editorMode: vm.editorMode()
-    });
-  };
-
-  self.clearResults = function() {
-    $.each(self.snippets(), (index, snippet) => {
-      snippet.result.clear();
-      snippet.status('ready');
-    });
-  };
-
-  self.executeAll = function() {
-    if (self.isExecutingAll() || self.snippets().length === 0) {
-      return;
-    }
-
-    self.isExecutingAll(true);
-    self.executingAllIndex(0);
-
-    self.snippets()[self.executingAllIndex()].execute();
-  };
-
-  self.saveDefaultUserProperties = function(session) {
-    apiHelper.saveConfiguration({
-      app: session.type(),
-      properties: session.properties,
-      userId: vm.userId
-    });
-  };
-
-  self.closeAndRemoveSession = function(session) {
-    self.closeSession(session, false, () => {
-      self.sessions.remove(session);
-    });
-  };
-
-  self.closeSession = function(session, silent, callback) {
-    $.post(
-      '/notebook/api/close_session',
-      {
-        session: komapping.toJSON(session)
-      },
-      data => {
-        if (!silent && data && data.status != 0 && data.status != -2 && data.message) {
-          $(document).trigger('error', data.message);
-        }
-
-        if (callback) {
-          callback();
-        }
-      }
-    ).fail(xhr => {
-      if (!silent && xhr.status !== 502) {
-        $(document).trigger('error', xhr.responseText);
-      }
-    });
-  };
-
-  self.fetchHistory = function(callback) {
-    const QUERIES_PER_PAGE = 50;
-    self.loadingHistory(true);
-
-    $.get(
-      '/notebook/api/get_history',
-      {
-        doc_type: self.selectedSnippet(),
-        limit: QUERIES_PER_PAGE,
-        page: self.historyCurrentPage(),
-        doc_text: self.historyFilter(),
-        is_notification_manager: vm.isNotificationManager()
-      },
-      data => {
-        const parsedHistory = [];
-        if (data && data.history) {
-          data.history.forEach(nbk => {
-            parsedHistory.push(
-              self._makeHistoryRecord(
-                nbk.absoluteUrl,
-                nbk.data.statement,
-                nbk.data.lastExecuted,
-                nbk.data.status,
-                nbk.name,
-                nbk.uuid
-              )
-            );
-          });
-        }
-        self.history(parsedHistory);
-        self.historyTotalPages(Math.ceil(data.count / QUERIES_PER_PAGE));
-      }
-    ).always(() => {
-      self.loadingHistory(false);
-      if (callback) {
-        callback();
-      }
-    });
-  };
-
-  self.prevHistoryPage = function() {
-    if (self.historyCurrentPage() !== 1) {
-      self.historyCurrentPage(self.historyCurrentPage() - 1);
-    }
-  };
-
-  self.nextHistoryPage = function() {
-    if (self.historyCurrentPage() < self.historyTotalPages()) {
-      self.historyCurrentPage(self.historyCurrentPage() + 1);
-    }
-  };
-
-  self.updateHistoryFailed = false;
-  self.updateHistory = function(statuses, interval) {
-    let items = $.grep(self.history(), item => {
-      return statuses.indexOf(item.status()) != -1;
-    }).slice(0, 25);
-
-    function updateHistoryCall(item) {
-      $.post('/notebook/api/check_status', {
-        notebook: komapping.toJSON({ id: item.uuid() })
-      })
-        .done(data => {
-          const status =
-            data.status == -3 ? 'expired' : data.status == 0 ? data.query_status.status : 'failed';
-          if (status && item.status() != status) {
-            item.status(status);
-          }
-        })
-        .fail(xhr => {
-          items = [];
-          self.updateHistoryFailed = true;
-          console.warn('Lost connectivity to the Hue history refresh backend.');
-        })
-        .always(() => {
-          if (items.length > 0) {
-            window.setTimeout(() => {
-              updateHistoryCall(items.pop());
-            }, 1000);
-          } else if (!self.updateHistoryFailed) {
-            window.setTimeout(() => {
-              self.updateHistory(statuses, interval);
-            }, interval);
-          }
-        });
-    }
-
-    if (items.length > 0) {
-      updateHistoryCall(items.pop());
-    } else if (!self.updateHistoryFailed) {
-      window.setTimeout(() => {
-        self.updateHistory(statuses, interval);
-      }, interval);
-    }
-  };
-
-  self._makeHistoryRecord = function(url, statement, lastExecuted, status, name, uuid) {
-    return komapping.fromJS({
-      url: url,
-      query: statement.substring(0, 1000) + (statement.length > 1000 ? '...' : ''),
-      lastExecuted: lastExecuted,
-      status: status,
-      name: name,
-      uuid: uuid
-    });
-  };
-
-  self.clearHistory = function(type) {
-    hueAnalytics.log('notebook', 'clearHistory');
-    $.post(
-      '/notebook/api/clear_history',
-      {
-        notebook: komapping.toJSON(self.getContext()),
-        doc_type: self.selectedSnippet(),
-        is_notification_manager: vm.isNotificationManager()
-      },
-      data => {
-        self.history.removeAll();
-        if (self.isHistory()) {
-          self.id(null);
-          self.uuid(hueUtils.UUID());
-          vm.changeURL(vm.URLS.editor + '?type=' + vm.editorType());
-        }
-      }
-    ).fail(xhr => {
-      if (xhr.status !== 502) {
-        $(document).trigger('error', xhr.responseText);
-      }
-    });
-    $(document).trigger('hideHistoryModal');
-  };
-
-  self.loadScheduler = function() {
-    if (typeof vm.CoordinatorEditorViewModel !== 'undefined' && self.isBatchable()) {
-      let _action;
-      if (self.coordinatorUuid()) {
-        _action = 'edit';
-      } else {
-        _action = 'new';
-      }
-      hueAnalytics.log('notebook', 'schedule/' + _action);
-
-      const getCoordinator = function() {
-        $.get(
-          '/oozie/editor/coordinator/' + _action + '/',
-          {
-            format: 'json',
-            document: self.uuid(),
-            coordinator: self.coordinatorUuid()
-          },
-          data => {
-            if ($('#schedulerEditor').length > 0) {
-              huePubSub.publish('hue4.process.headers', {
-                response: data.layout,
-                callback: function(r) {
-                  $('#schedulerEditor').html(r);
-
-                  self.schedulerViewModel = new vm.CoordinatorEditorViewModel(
-                    data.coordinator,
-                    data.credentials,
-                    data.workflows,
-                    data.can_edit
-                  );
-
-                  ko.cleanNode($('#schedulerEditor')[0]);
-                  ko.applyBindings(self.schedulerViewModel, $('#schedulerEditor')[0]);
-                  $(document).off('showSubmitPopup');
-                  $(document).on('showSubmitPopup', (event, data) => {
-                    $('.submit-modal-editor').html(data);
-                    $('.submit-modal-editor').modal('show');
-                    $('.submit-modal-editor').on('hidden', () => {
-                      huePubSub.publish('hide.datepicker');
-                    });
-                    const _sel = $('.submit-form .control-group[rel!="popover"]:visible');
-                    if (_sel.length > 0) {
-                      $('.submit-modal-editor .modal-body').height(
-                        $('.submit-modal-editor .modal-body').height() + 60
-                      );
-                    }
-                  });
-
-                  huePubSub.publish('render.jqcron');
-
-                  self.schedulerViewModel.coordinator.properties.cron_advanced.valueHasMutated(); // Update jsCron enabled status
-                  self.schedulerViewModel.coordinator.tracker().markCurrentStateAsClean();
-                  self.schedulerViewModel.isEditing(true);
-
-                  self.schedulerViewModelIsLoaded(true);
-
-                  if (_action == 'new') {
-                    self.schedulerViewModel.coordinator.properties.document(self.uuid()); // Expected for triggering the display
-                  }
-                }
-              });
-            }
-          }
-        ).fail(xhr => {
-          if (xhr.status !== 502) {
-            $(document).trigger('error', xhr.responseText);
-          }
-        });
-      };
-
-      getCoordinator();
-    }
-  };
-
-  self.saveScheduler = function() {
-    if (
-      self.isBatchable() &&
-      (!self.coordinatorUuid() || self.schedulerViewModel.coordinator.isDirty())
-    ) {
-      self.schedulerViewModel.coordinator.isManaged(true);
-      self.schedulerViewModel.coordinator.properties.document(self.uuid());
-      self.schedulerViewModel.save(data => {
-        if (!self.coordinatorUuid()) {
-          self.coordinatorUuid(data.uuid);
-          self.save();
-        }
-      });
-    }
-  };
-
-  self.showSubmitPopup = function() {
-    $.get(
-      '/oozie/editor/coordinator/submit/' + self.coordinatorUuid(),
-      {
-        format: 'json'
-      },
-      data => {
-        $(document).trigger('showSubmitPopup', data);
-      }
-    ).fail((xhr, textStatus, errorThrown) => {
-      if (xhr.status !== 502) {
-        $(document).trigger('error', xhr.responseText);
-      }
-    });
-  };
-
-  self.viewSchedulerId = ko.observable(
-    typeof notebook.viewSchedulerId != 'undefined' && notebook.viewSchedulerId != null
-      ? notebook.viewSchedulerId
-      : ''
-  );
-  self.viewSchedulerId.subscribe(newVal => {
-    self.save();
-  });
-  self.isSchedulerJobRunning = ko.observable();
-  self.loadingScheduler = ko.observable(false);
-
-  // Init
-  if (notebook.snippets) {
-    $.each(notebook.snippets, (index, snippet) => {
-      self.addSnippet(snippet);
-    });
-    if (
-      typeof notebook.presentationSnippets != 'undefined' &&
-      notebook.presentationSnippets != null
-    ) {
-      // Load
-      $.each(notebook.presentationSnippets, (key, snippet) => {
-        snippet.status = 'ready'; // Protect from storm of check_statuses
-        const _snippet = new Snippet(vm, self, snippet);
-        _snippet.init();
-        _snippet.previousChartOptions = vm._getPreviousChartOptions(_snippet);
-        self.presentationSnippets()[key] = _snippet;
-      });
-    }
-    if (vm.editorMode() && self.history().length == 0) {
-      self.fetchHistory(() => {
-        self.updateHistory(['starting', 'running'], 30000);
-        self.updateHistory(['available'], 60000 * 5);
-      });
-    }
-  }
-
-  huePubSub.subscribeOnce(
-    'assist.db.panel.ready',
-    () => {
-      if (self.type().indexOf('query') === 0) {
-        const whenDatabaseAvailable = function(snippet) {
-          huePubSub.publish('assist.set.database', {
-            source: snippet.type(),
-            namespace: snippet.namespace(),
-            name: snippet.database()
-          });
-        };
-
-        const whenNamespaceAvailable = function(snippet) {
-          if (snippet.database()) {
-            whenDatabaseAvailable(snippet);
-          } else {
-            const databaseSub = snippet.database.subscribe(() => {
-              databaseSub.dispose();
-              whenDatabaseAvailable(snippet);
-            });
-          }
-        };
-
-        const whenSnippetAvailable = function(snippet) {
-          if (snippet.namespace()) {
-            whenNamespaceAvailable(snippet);
-          } else {
-            const namespaceSub = snippet.namespace.subscribe(() => {
-              namespaceSub.dispose();
-              whenNamespaceAvailable(snippet);
-            });
-          }
-        };
-
-        if (self.snippets().length === 1) {
-          whenSnippetAvailable(self.snippets()[0]);
-        } else {
-          const snippetsSub = self.snippets.subscribe(snippets => {
-            if (snippets.length === 1) {
-              whenSnippetAvailable(snippets[0]);
-            }
-            snippetsSub.dispose();
-          });
-        }
-      }
-    },
-    vm.huePubSubId
-  );
-
-  huePubSub.publish('assist.is.db.panel.ready');
-};
-
-function EditorViewModel(
-  editor_id,
-  notebooks,
-  options,
-  CoordinatorEditorViewModel,
-  RunningCoordinatorModel
-) {
-  const self = this;
-
-  self.URLS = {
-    editor: '/hue/editor',
-    notebook: '/hue/notebook',
-    report: '/hue/dashboard/new_search?engine=report'
-  };
-
-  self.huePubSubId = options.huePubSubId || 'editor';
-  self.user = options.user;
-  self.userId = options.userId;
-  self.suffix = options.suffix;
-  self.isMobile = ko.observable(options.mobile);
-  self.isNotificationManager = ko.observable(options.is_notification_manager || false);
-  self.editorType = ko.observable(options.editor_type);
-  self.editorType.subscribe(newVal => {
-    self.editorMode(newVal != 'notebook');
-    hueUtils.changeURLParameter('type', newVal);
-    if (self.editorMode()) {
-      self.selectedNotebook().fetchHistory(); // Js error if notebook did not have snippets
-    }
-  });
-  self.preEditorTogglingSnippet = ko.observable();
-  self.toggleEditorMode = function() {
-    const _notebook = self.selectedNotebook();
-    const _newSnippets = [];
-
-    if (self.editorType() != 'notebook') {
-      self.editorType('notebook');
-      self.preEditorTogglingSnippet(_notebook.snippets()[0]);
-      const _variables = _notebook.snippets()[0].variables();
-      const _statementKeys = [];
-      // Split statements
-      _notebook.type('notebook');
-      _notebook
-        .snippets()[0]
-        .statementsList()
-        .forEach(sql_statement => {
-          let _snippet;
-          if (sql_statement.hashCode() in _notebook.presentationSnippets()) {
-            _snippet = _notebook.presentationSnippets()[sql_statement.hashCode()]; // Persist result
-            _snippet.variables(_variables);
-          } else {
-            const _title = [];
-            const _statement = [];
-            sql_statement
-              .trim()
-              .split('\n')
-              .forEach(line => {
-                if (line.trim().startsWith('--') && _statement.length === 0) {
-                  _title.push(line.substr(2));
-                } else {
-                  _statement.push(line);
-                }
-              });
-            _snippet = new Snippet(self, _notebook, {
-              type: _notebook.initialType,
-              statement_raw: _statement.join('\n'),
-              result: {},
-              name: _title.join('\n'),
-              variables: komapping.toJS(_variables)
-            });
-            _snippet.variables = _notebook.snippets()[0].variables;
-            _snippet.init();
-            _notebook.presentationSnippets()[sql_statement.hashCode()] = _snippet;
-          }
-          _statementKeys.push(sql_statement.hashCode());
-          _newSnippets.push(_snippet);
-        });
-      $.each(_notebook.presentationSnippets(), key => {
-        // Dead statements
-        if (!key in _statementKeys) {
-          delete _notebook.presentationSnippets()[key];
-        }
-      });
-    } else {
-      self.editorType(_notebook.initialType);
-      // Revert to one statement
-      _newSnippets.push(self.preEditorTogglingSnippet());
-      _notebook.type('query-' + _notebook.initialType);
-    }
-    _notebook.snippets(_newSnippets);
-    _newSnippets.forEach(snippet => {
-      huePubSub.publish('editor.redraw.data', { snippet: snippet });
-    });
-  };
-  self.togglePresentationMode = function() {
-    if (self.selectedNotebook().initialType !== 'notebook') {
-      self.toggleEditorMode();
-    }
-  };
-  self.editorTypeTitle = ko.pureComputed(() => {
-    const foundInterpreter = $.grep(options.languages, interpreter => {
-      return interpreter.type === self.editorType();
-    });
-    return foundInterpreter.length > 0 ? foundInterpreter[0].name : self.editorType();
-  });
-  self.autocompleteTimeout = options.autocompleteTimeout;
-  self.selectedNotebook = ko.observable();
-
-  self.combinedContent = ko.observable();
-  self.isPresentationModeEnabled = ko.pureComputed(() => {
-    return (
-      self.selectedNotebook() &&
-      self.selectedNotebook().snippets().length === 1 &&
-      self
-        .selectedNotebook()
-        .snippets()[0]
-        .isSqlDialect()
-    );
-  });
-  self.isResultFullScreenMode = ko.observable(false);
-  self.isPresentationMode = ko.computed(() => {
-    return self.selectedNotebook() && self.selectedNotebook().isPresentationMode();
-  });
-  self.isHidingCode = ko.computed(() => {
-    return self.selectedNotebook() && self.selectedNotebook().isHidingCode();
-  });
-  self.successUrl = ko.observable(options.success_url); // Deprecated
-  self.isOptimizerEnabled = ko.observable(options.is_optimizer_enabled);
-  self.isNavigatorEnabled = ko.observable(options.is_navigator_enabled);
-
-  self.CoordinatorEditorViewModel = CoordinatorEditorViewModel;
-  self.RunningCoordinatorModel = RunningCoordinatorModel;
-
-  self.canSave = ko.computed(() => {
-    // Saved query or history but history coming from a saved query
-    return (
-      self.selectedNotebook() &&
-      self.selectedNotebook().canWrite() &&
-      (self.selectedNotebook().isSaved() ||
-        (self.selectedNotebook().isHistory() && self.selectedNotebook().parentSavedQueryUuid()))
-    );
-  });
-
-  // TODO: Drop the SQL source types from the notebook. They're now set in AssistDbPanel.
-  self.sqlSourceTypes = [];
-  self.availableLanguages = [];
-
-  if (options.languages && options.snippetViewSettings) {
-    $.each(options.languages, (idx, language) => {
-      self.availableLanguages.push({
-        type: language.type,
-        name: language.name,
-        interface: language.interface
-      });
-      const viewSettings = options.snippetViewSettings[language.type];
-      if (viewSettings && viewSettings.sqlDialect) {
-        self.sqlSourceTypes.push({
-          type: language.type,
-          name: language.name
-        });
-      }
-    });
-  }
-
-  const sqlSourceTypes = $.grep(self.sqlSourceTypes, language => {
-    return language.type == self.editorType();
-  });
-  if (sqlSourceTypes.length > 0) {
-    self.activeSqlSourceType = sqlSourceTypes[0].type;
-  } else {
-    self.activeSqlSourceType = null;
-  }
-
-  self.displayCombinedContent = function() {
-    if (!self.selectedNotebook()) {
-      self.combinedContent('');
-    } else {
-      let statements = '';
-      $.each(self.selectedNotebook().snippets(), (index, snippet) => {
-        if (snippet.statement()) {
-          if (statements) {
-            statements += '\n\n';
-          }
-          statements += snippet.statement();
-        }
-      });
-      self.combinedContent(statements);
-    }
-    $('#combinedContentModal' + self.suffix).modal('show');
-  };
-
-  self.isEditing = ko.observable(false);
-  self.isEditing.subscribe(() => {
-    $(document).trigger('editingToggled');
-  });
-  self.toggleEditing = function() {
-    self.isEditing(!self.isEditing());
-  };
-
-  self.authSessionUsername = ko.observable(); // UI popup
-  self.authSessionPassword = ko.observable();
-  self.authSessionType = ko.observable();
-  self.authSessionCallback = ko.observable();
-
-  self.removeSnippetConfirmation = ko.observable();
-
-  self.removeSnippet = function(notebook, snippet) {
-    let hasContent = snippet.statement_raw().length > 0;
-    if (!hasContent) {
-      $.each(snippet.properties(), (key, value) => {
-        hasContent = hasContent || (ko.isObservable(value) && value().length > 0);
-      });
-    }
-    if (hasContent) {
-      self.removeSnippetConfirmation({ notebook: notebook, snippet: snippet });
-      $('#removeSnippetModal' + self.suffix).modal('show');
-    } else {
-      notebook.snippets.remove(snippet);
-      window.setTimeout(() => {
-        $(document).trigger('editorSizeChanged');
-      }, 100);
-    }
-  };
-
-  self.assistAvailable = ko.observable(options.assistAvailable);
-
-  self.assistWithoutStorage = ko.observable(false);
-
-  self.isLeftPanelVisible = ko.observable(
-    apiHelper.getFromTotalStorage('assist', 'assist_panel_visible', true)
-  );
-  self.isLeftPanelVisible.subscribe(val => {
-    if (!self.assistWithoutStorage()) {
-      apiHelper.setInTotalStorage('assist', 'assist_panel_visible', val);
-    }
-  });
-
-  self.isRightPanelAvailable = ko.observable(options.assistAvailable && HAS_OPTIMIZER);
-  self.isRightPanelVisible = ko.observable(
-    apiHelper.getFromTotalStorage('assist', 'right_assist_panel_visible', true)
-  );
-  self.isRightPanelVisible.subscribe(val => {
-    if (!self.assistWithoutStorage()) {
-      apiHelper.setInTotalStorage('assist', 'right_assist_panel_visible', val);
-    }
-  });
-
-  const withActiveSnippet = function(callback) {
-    const notebook = self.selectedNotebook();
-    let foundSnippet;
-    if (notebook) {
-      if (notebook.snippets().length === 1) {
-        foundSnippet = notebook.snippets()[0];
-      } else {
-        notebook.snippets().every(snippet => {
-          if (snippet.inFocus()) {
-            foundSnippet = snippet;
-            return false;
-          }
-          return true;
-        });
-      }
-    }
-    if (foundSnippet) {
-      callback(foundSnippet);
-    }
-  };
-
-  huePubSub.subscribe('assist.highlight.risk.suggestions', () => {
-    if (self.isRightPanelAvailable() && !self.isRightPanelVisible()) {
-      self.isRightPanelVisible(true);
-    }
-  });
-
-  self.isContextPanelVisible = ko.observable(false);
-  self.isContextPanelVisible.subscribe(newValue => {
-    huePubSub.publish('context.panel.visible', newValue);
-  });
-
-  huePubSub.subscribe('context.panel.visible.editor', self.isContextPanelVisible);
-
-  huePubSub.subscribe(
-    'get.active.snippet.type',
-    () => {
-      withActiveSnippet(activeSnippet => {
-        huePubSub.publish('set.active.snippet.type', activeSnippet.type());
-      });
-    },
-    self.huePubSubId
-  );
-
-  huePubSub.subscribe(
-    'save.snippet.to.file',
-    () => {
-      withActiveSnippet(activeSnippet => {
-        const data = {
-          path: activeSnippet.statementPath(),
-          contents: activeSnippet.statement()
-        };
-        const options = {
-          successCallback: function(result) {
-            if (result && result.exists) {
-              $(document).trigger('info', result.path + ' saved successfully.');
-            } else {
-              self._ajaxError(result);
-            }
-          }
-        };
-        apiHelper.saveSnippetToFile(data, options);
-      });
-    },
-    self.huePubSubId
-  );
-
-  huePubSub.subscribe(
-    'sql.context.pin',
-    contextData => {
-      withActiveSnippet(activeSnippet => {
-        contextData.tabId = 'context' + activeSnippet.pinnedContextTabs().length;
-        activeSnippet.pinnedContextTabs.push(contextData);
-        activeSnippet.currentQueryTab(contextData.tabId);
-      });
-    },
-    self.huePubSubId
-  );
-
-  huePubSub.subscribe(
-    'assist.database.set',
-    databaseDef => {
-      withActiveSnippet(activeSnippet => {
-        activeSnippet.handleAssistSelection(databaseDef);
-      });
-    },
-    self.huePubSubId
-  );
-
-  huePubSub.subscribe(
-    'assist.database.selected',
-    databaseDef => {
-      withActiveSnippet(activeSnippet => {
-        activeSnippet.handleAssistSelection(databaseDef);
-      });
-    },
-    self.huePubSubId
-  );
-
-  self.availableSnippets = komapping.fromJS(options.languages);
-
-  self.editorMode = ko.observable(options.mode == 'editor');
-
-  self.getSnippetViewSettings = function(snippetType) {
-    if (options.snippetViewSettings[snippetType]) {
-      return options.snippetViewSettings[snippetType];
-    }
-    return options.snippetViewSettings.default;
-  };
-
-  self.availableSessionProperties = ko.computed(() => {
-    // Only Spark
-    return ko.utils.arrayFilter(options.session_properties, item => {
-      return item.name != ''; // Could filter out the ones already selected + yarn only or not
-    });
-  });
-  self.getSessionProperties = function(name) {
-    let _prop = null;
-    $.each(options.session_properties, (index, prop) => {
-      if (prop.name == name) {
-        _prop = prop;
-        return;
-      }
-    });
-    return _prop;
-  };
-
-  self.getSnippetName = function(snippetType) {
-    const availableSnippets = self.availableSnippets();
-    for (let i = 0; i < availableSnippets.length; i++) {
-      if (availableSnippets[i].type() === snippetType) {
-        return availableSnippets[i].name();
-      }
-    }
-    return '';
-  };
-
-  self.changeURL = function(url) {
-    if (!self.isNotificationManager()) {
-      hueUtils.changeURL(url);
-    }
-  };
-
-  self.init = function() {
-    if (editor_id) {
-      self.openNotebook(editor_id);
-    } else if (window.location.getParameter('editor') !== '') {
-      self.openNotebook(window.location.getParameter('editor'));
-    } else if (notebooks.length > 0) {
-      self.loadNotebook(notebooks[0]); // Old way of loading json for /browse
-    } else if (window.location.getParameter('type') !== '') {
-      self.newNotebook(window.location.getParameter('type'));
-    } else {
-      self.newNotebook();
-    }
-  };
-
-  self.loadNotebook = function(notebookRaw, queryTab) {
-    let currentQueries;
-    if (self.selectedNotebook() != null) {
-      currentQueries = self.selectedNotebook().unload();
-    }
-
-    const notebook = new Notebook(self, notebookRaw);
-
-    if (notebook.snippets().length > 0) {
-      huePubSub.publish('detach.scrolls', notebook.snippets()[0]);
-      notebook.selectedSnippet(notebook.snippets()[notebook.snippets().length - 1].type());
-      if (currentQueries != null) {
-        notebook.snippets()[0].queries(currentQueries);
-      }
-      notebook.snippets().forEach(snippet => {
-        snippet.aceAutoExpand = false;
-        snippet.statement_raw.valueHasMutated();
-        if (
-          snippet.result.handle().statements_count > 1 &&
-          snippet.result.handle().start != null &&
-          snippet.result.handle().end != null
-        ) {
-          const aceLineOffset = snippet.result.handle().aceLineOffset || 0;
-          snippet.result.statement_range({
-            start: {
-              row: snippet.result.handle().start.row + aceLineOffset,
-              column: snippet.result.handle().start.column
-            },
-            end: {
-              row: snippet.result.handle().end.row + aceLineOffset,
-              column: snippet.result.handle().end.column
-            }
-          });
-          snippet.result.statement_range.valueHasMutated();
-        }
-
-        snippet.previousChartOptions = self._getPreviousChartOptions(snippet);
-      });
-
-      if (notebook.snippets()[0].result.data().length > 0) {
-        $(document).trigger('redrawResults');
-      } else if (queryTab) {
-        notebook.snippets()[0].currentQueryTab(queryTab);
-      }
-
-      if (notebook.isSaved()) {
-        notebook.snippets()[0].currentQueryTab('savedQueries');
-        if (notebook.snippets()[0].queries().length === 0) {
-          notebook.snippets()[0].fetchQueries(); // Subscribe not updating yet
-        }
-      }
-    }
-
-    self.selectedNotebook(notebook);
-    huePubSub.publish('check.job.browser');
-    huePubSub.publish('recalculate.name.description.width');
-  };
-
-  self._getPreviousChartOptions = function(snippet) {
-    return {
-      chartLimit:
-        typeof snippet.chartLimit() !== 'undefined'
-          ? snippet.chartLimit()
-          : snippet.previousChartOptions.chartLimit,
-      chartX:
-        typeof snippet.chartX() !== 'undefined'
-          ? snippet.chartX()
-          : snippet.previousChartOptions.chartX,
-      chartXPivot:
-        typeof snippet.chartXPivot() !== 'undefined'
-          ? snippet.chartXPivot()
-          : snippet.previousChartOptions.chartXPivot,
-      chartYSingle:
-        typeof snippet.chartYSingle() !== 'undefined'
-          ? snippet.chartYSingle()
-          : snippet.previousChartOptions.chartYSingle,
-      chartMapType:
-        typeof snippet.chartMapType() !== 'undefined'
-          ? snippet.chartMapType()
-          : snippet.previousChartOptions.chartMapType,
-      chartMapLabel:
-        typeof snippet.chartMapLabel() !== 'undefined'
-          ? snippet.chartMapLabel()
-          : snippet.previousChartOptions.chartMapLabel,
-      chartMapHeat:
-        typeof snippet.chartMapHeat() !== 'undefined'
-          ? snippet.chartMapHeat()
-          : snippet.previousChartOptions.chartMapHeat,
-      chartYMulti:
-        typeof snippet.chartYMulti() !== 'undefined'
-          ? snippet.chartYMulti()
-          : snippet.previousChartOptions.chartYMulti,
-      chartScope:
-        typeof snippet.chartScope() !== 'undefined'
-          ? snippet.chartScope()
-          : snippet.previousChartOptions.chartScope,
-      chartTimelineType:
-        typeof snippet.chartTimelineType() !== 'undefined'
-          ? snippet.chartTimelineType()
-          : snippet.previousChartOptions.chartTimelineType,
-      chartSorting:
-        typeof snippet.chartSorting() !== 'undefined'
-          ? snippet.chartSorting()
-          : snippet.previousChartOptions.chartSorting,
-      chartScatterGroup:
-        typeof snippet.chartScatterGroup() !== 'undefined'
-          ? snippet.chartScatterGroup()
-          : snippet.previousChartOptions.chartScatterGroup,
-      chartScatterSize:
-        typeof snippet.chartScatterSize() !== 'undefined'
-          ? snippet.chartScatterSize()
-          : snippet.previousChartOptions.chartScatterSize
-    };
-  };
-
-  self.openNotebook = function(uuid, queryTab, skipUrlChange, callback) {
-    const deferredOpen = new $.Deferred();
-    $.get(
-      '/desktop/api2/doc/',
-      {
-        uuid: uuid,
-        data: true,
-        dependencies: true
-      },
-      data => {
-        if (data.status == 0) {
-          data.data.dependents = data.dependents;
-          data.data.can_write = data.user_perms.can_write;
-          const notebook = data.data;
-          self.loadNotebook(notebook, queryTab);
-          if (typeof skipUrlChange === 'undefined' && !self.isNotificationManager()) {
-            if (self.editorMode()) {
-              self.editorType(data.document.type.substring('query-'.length));
-              huePubSub.publish('active.snippet.type.changed', self.editorType());
-              self.changeURL(
-                self.URLS.editor + '?editor=' + data.document.id + '&type=' + self.editorType()
-              );
-            } else {
-              self.changeURL(self.URLS.notebook + '?notebook=' + data.document.id);
-            }
-          }
-          if (typeof callback !== 'undefined') {
-            callback();
-          }
-          deferredOpen.resolve();
-        } else {
-          $(document).trigger('error', data.message);
-          deferredOpen.reject();
-          self.newNotebook();
-        }
-      }
-    );
-    return deferredOpen.promise();
-  };
-
-  self.newNotebook = function(editorType, callback, queryTab) {
-    huePubSub.publish('active.snippet.type.changed', editorType);
-    $.post(
-      '/notebook/api/create_notebook',
-      {
-        type: editorType || options.editor_type,
-        directory_uuid: window.location.getParameter('directory_uuid')
-      },
-      data => {
-        self.loadNotebook(data.notebook);
-        if (self.editorMode() && !self.isNotificationManager()) {
-          const snippet = self.selectedNotebook().newSnippet(self.editorType());
-          if (
-            queryTab &&
-            ['queryHistory', 'savedQueries', 'queryBuilderTab'].indexOf(queryTab) > -1
-          ) {
-            snippet.currentQueryTab(queryTab);
-          }
-          huePubSub.publish('detach.scrolls', self.selectedNotebook().snippets()[0]);
-          if (window.location.getParameter('type') === '') {
-            hueUtils.changeURLParameter('type', self.editorType());
-          }
-          huePubSub.publish('active.snippet.type.changed', editorType);
-        }
-
-        if (typeof callback !== 'undefined' && callback !== null) {
-          callback();
-        }
-      }
-    );
-  };
-
-  self.saveNotebook = function() {
-    self.selectedNotebook().save();
-  };
-
-  self.saveAsNotebook = function() {
-    self.selectedNotebook().id(null);
-    self.selectedNotebook().uuid(hueUtils.UUID());
-    self.selectedNotebook().parentSavedQueryUuid(null);
-    self.selectedNotebook().save(() => {
-      huePubSub.publish('assist.document.refresh');
-    });
-  };
-
-  self.showContextPopover = function(field, event) {
-    const $source = $(
-      event.target && event.target.nodeName !== 'A' ? event.target.parentElement : event.target
-    );
-    const offset = $source.offset();
-    huePubSub.publish('context.popover.show', {
-      data: {
-        type: 'catalogEntry',
-        catalogEntry: field.catalogEntry
-      },
-      onSampleClick: field.value,
-      showInAssistEnabled: true,
-      sourceType: self.editorType(),
-      orientation: 'bottom',
-      defaultDatabase: 'default',
-      pinEnabled: false,
-      source: {
-        element: event.target,
-        left: offset.left,
-        top: offset.top - 3,
-        right: offset.left + $source.width() + 1,
-        bottom: offset.top + $source.height() - 3
-      }
-    });
-  };
-}
-
-export default EditorViewModel;

+ 320 - 0
desktop/core/src/desktop/js/apps/notebook/result.js

@@ -0,0 +1,320 @@
+// 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';
+
+class Result {
+  constructor(snippet, result) {
+    const self = this;
+
+    $.extend(
+      snippet,
+      snippet.chartType == 'lines' && {
+        // Retire line chart
+        chartType: 'bars',
+        chartTimelineType: 'line'
+      }
+    );
+    self.id = ko.observable(
+      typeof result.id != 'undefined' && result.id != null ? result.id : hueUtils.UUID()
+    );
+    self.type = ko.observable(
+      typeof result.type != 'undefined' && result.type != null ? result.type : 'table'
+    );
+    self.hasResultset = ko
+      .observable(
+        typeof result.hasResultset != 'undefined' && result.hasResultset != null
+          ? result.hasResultset
+          : true
+      )
+      .extend('throttle', 100);
+    self.handle = ko.observable(
+      typeof result.handle != 'undefined' && result.handle != null ? result.handle : {}
+    );
+    self.meta = ko.observableArray(
+      typeof result.meta != 'undefined' && result.meta != null ? result.meta : []
+    );
+
+    const adaptMeta = function() {
+      let i = 0;
+      self.meta().forEach(item => {
+        if (typeof item.checked === 'undefined') {
+          item.checked = ko.observable(true);
+          item.checked.subscribe(() => {
+            self.filteredMetaChecked(
+              self.filteredMeta().some(item => {
+                return item.checked();
+              })
+            );
+          });
+        }
+        item.type = item.type.replace(/_type/i, '').toLowerCase();
+        if (typeof item.originalIndex === 'undefined') {
+          item.originalIndex = i;
+        }
+        i++;
+      });
+    };
+
+    adaptMeta();
+    self.meta.subscribe(adaptMeta);
+
+    self.rows = ko.observable(
+      typeof result.rows != 'undefined' && result.rows != null ? result.rows : null
+    );
+    self.hasMore = ko.observable(
+      typeof result.hasMore != 'undefined' && result.hasMore != null ? result.hasMore : false
+    );
+    self.statement_id = ko.observable(
+      typeof result.statement_id != 'undefined' && result.statement_id != null
+        ? result.statement_id
+        : 0
+    );
+    self.statement_range = ko.observable(
+      typeof result.statement_range != 'undefined' && result.statement_range != null
+        ? result.statement_range
+        : {
+            start: {
+              row: 0,
+              column: 0
+            },
+            end: {
+              row: 0,
+              column: 0
+            }
+          }
+    );
+    // We don't keep track of any previous selection so prevent entering into batch execution mode after load by setting
+    // statements_count to 1. For the case when a selection is not starting at row 0.
+    self.statements_count = ko.observable(1);
+    self.previous_statement_hash = ko.observable(
+      typeof result.previous_statement_hash != 'undefined' && result.previous_statement_hash != null
+        ? result.previous_statement_hash
+        : null
+    );
+    self.cleanedMeta = ko.computed(() => {
+      return ko.utils.arrayFilter(self.meta(), item => {
+        return item.name != '';
+      });
+    });
+    self.metaFilter = ko.observable();
+
+    self.isMetaFilterVisible = ko.observable(false);
+    self.filteredMetaChecked = ko.observable(true);
+    self.filteredMeta = ko.pureComputed(() => {
+      if (!self.metaFilter() || self.metaFilter().query === '') {
+        return self.meta();
+      }
+
+      return self.meta().filter(item => {
+        const facets = self.metaFilter().facets;
+        const isFacetMatch = !facets || Object.keys(facets).length === 0 || !facets['type']; // So far only type facet is used for SQL
+        const isTextMatch = !self.metaFilter().text || self.metaFilter().text.length === 0;
+        let match = true;
+
+        if (!isFacetMatch) {
+          match = !!facets['type'][item.type];
+        }
+
+        if (match && !isTextMatch) {
+          match = self.metaFilter().text.every(text => {
+            return item.name.toLowerCase().indexOf(text.toLowerCase()) !== -1;
+          });
+        }
+        return match;
+      });
+    });
+
+    self.autocompleteFromEntries = function(nonPartial, partial) {
+      const result = [];
+      const partialLower = partial.toLowerCase();
+      self.meta().forEach(column => {
+        if (column.name.toLowerCase().indexOf(partialLower) === 0) {
+          result.push(nonPartial + partial + column.name.substring(partial.length));
+        } else if (column.name.toLowerCase().indexOf('.' + partialLower) !== -1) {
+          result.push(
+            nonPartial +
+              partial +
+              column.name.substring(
+                partial.length + column.name.toLowerCase().indexOf('.' + partialLower) + 1
+              )
+          );
+        }
+      });
+
+      return result;
+    };
+    self.clickFilteredMetaCheck = function() {
+      self.filteredMeta().forEach(item => {
+        item.checked(self.filteredMetaChecked());
+      });
+    };
+
+    self.fetchedOnce = ko.observable(
+      typeof result.fetchedOnce != 'undefined' && result.fetchedOnce != null
+        ? result.fetchedOnce
+        : false
+    );
+    self.startTime = ko.observable(
+      typeof result.startTime != 'undefined' && result.startTime != null
+        ? new Date(result.startTime)
+        : new Date()
+    );
+    self.endTime = ko.observable(
+      typeof result.endTime != 'undefined' && result.endTime != null
+        ? new Date(result.endTime)
+        : new Date()
+    );
+    self.executionTime = ko.computed(() => {
+      return self.endTime().getTime() - self.startTime().getTime();
+    });
+
+    function isNumericColumn(type) {
+      return (
+        $.inArray(type, [
+          'tinyint',
+          'smallint',
+          'int',
+          'bigint',
+          'float',
+          'double',
+          'decimal',
+          'real'
+        ]) > -1
+      );
+    }
+
+    function isDateTimeColumn(type) {
+      return $.inArray(type, ['timestamp', 'date', 'datetime']) > -1;
+    }
+
+    function isComplexColumn(type) {
+      return $.inArray(type, ['array', 'map', 'struct']) > -1;
+    }
+
+    function isStringColumn(type) {
+      return !isNumericColumn(type) && !isDateTimeColumn(type) && !isComplexColumn(type);
+    }
+
+    self.cleanedNumericMeta = ko.computed(() => {
+      return ko.utils.arrayFilter(self.meta(), item => {
+        return item.name != '' && isNumericColumn(item.type);
+      });
+    });
+
+    self.cleanedStringMeta = ko.computed(() => {
+      return ko.utils.arrayFilter(self.meta(), item => {
+        return item.name != '' && isStringColumn(item.type);
+      });
+    });
+
+    self.cleanedDateTimeMeta = ko.computed(() => {
+      return ko.utils.arrayFilter(self.meta(), item => {
+        return item.name != '' && isDateTimeColumn(item.type);
+      });
+    });
+
+    self.data = ko.observableArray(
+      typeof result.data != 'undefined' && result.data != null ? result.data : []
+    );
+    self.data.extend({ rateLimit: 50 });
+    self.explanation = ko.observable(
+      typeof result.explanation != 'undefined' && result.explanation != null
+        ? result.explanation
+        : ''
+    );
+    self.images = ko.observableArray(
+      typeof result.images != 'undefined' && result.images != null ? result.images : []
+    );
+    self.images.extend({ rateLimit: 50 });
+    self.logs = ko.observable('');
+    self.logLines = 0;
+    self.hasSomeResults = ko.computed(() => {
+      return self.hasResultset() && self.data().length > 0; // status() == 'available'
+    });
+
+    self.getContext = function() {
+      return {
+        id: self.id,
+        type: self.type,
+        handle: self.handle
+      };
+    };
+
+    self.clear = function() {
+      self.fetchedOnce(false);
+      self.hasMore(false);
+      self.statement_range({
+        start: {
+          row: 0,
+          column: 0
+        },
+        end: {
+          row: 0,
+          column: 0
+        }
+      });
+      self.meta.removeAll();
+      self.data.removeAll();
+      self.images.removeAll();
+      self.logs('');
+      self.handle({
+        // Keep multiquery indexing
+        has_more_statements: self.handle()['has_more_statements'],
+        statement_id: self.handle()['statement_id'],
+        statements_count: self.handle()['statements_count'],
+        previous_statement_hash: self.handle()['previous_statement_hash']
+      });
+      self.startTime(new Date());
+      self.endTime(new Date());
+      self.explanation('');
+      self.logLines = 0;
+      self.rows(null);
+    };
+  }
+
+  cancelBatchExecution() {
+    const self = this;
+    self.statements_count(1);
+    self.hasMore(false);
+    self.statement_range({
+      start: {
+        row: 0,
+        column: 0
+      },
+      end: {
+        row: 0,
+        column: 0
+      }
+    });
+    self.handle()['statement_id'] = 0;
+    self.handle()['start'] = {
+      row: 0,
+      column: 0
+    };
+    self.handle()['end'] = {
+      row: 0,
+      column: 0
+    };
+    self.handle()['has_more_statements'] = false;
+    self.handle()['previous_statement_hash'] = '';
+  }
+}
+
+export default Result;

+ 47 - 0
desktop/core/src/desktop/js/apps/notebook/session.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';
+import komapping from 'knockout.mapping';
+
+class Session {
+  constructor(vm, session) {
+    const self = this;
+    komapping.fromJS(session, {}, self);
+
+    self.selectedSessionProperty = ko.observable('');
+
+    self.restarting = ko.observable(false);
+
+    if (!ko.isObservable(self.properties)) {
+      self.properties = ko.observableArray();
+    }
+
+    self.availableNewProperties = ko.computed(() => {
+      const addedIndex = {};
+      $.each(self.properties(), (index, property) => {
+        addedIndex[property.key] = true;
+      });
+      const result = $.grep(vm.availableSessionProperties(), property => {
+        return !addedIndex[property.name];
+      });
+      return result;
+    });
+  }
+}
+
+export default Session;

+ 2693 - 0
desktop/core/src/desktop/js/apps/notebook/snippet.js

@@ -0,0 +1,2693 @@
+// 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 komapping from 'knockout.mapping';
+
+import AceAutocompleteWrapper from 'apps/notebook/aceAutocompleteWrapper';
+import apiHelper from 'api/apiHelper';
+import dataCatalog from 'catalog/dataCatalog';
+import hueAnalytics from 'utils/hueAnalytics';
+import huePubSub from 'utils/huePubSub';
+import hueUtils from 'utils/hueUtils';
+import { markdown } from 'markdown';
+
+import Session from 'apps/notebook/session';
+import Result from 'apps/notebook/result';
+
+const NOTEBOOK_MAPPING = {
+  ignore: [
+    'ace',
+    'aceMode',
+    'autocompleter',
+    'availableDatabases',
+    'availableSnippets',
+    'avoidClosing',
+    'canWrite',
+    'cleanedDateTimeMeta',
+    'cleanedMeta',
+    'cleanedNumericMeta',
+    'cleanedStringMeta',
+    'dependents',
+    'errorLoadingQueries',
+    'hasProperties',
+    'history',
+    'images',
+    'inFocus',
+    'queries',
+    'saveResultsModalVisible',
+    'selectedStatement',
+    'snippetImage',
+    'user',
+    'positionStatement',
+    'lastExecutedStatement',
+    'downloadResultViewModel'
+  ]
+};
+
+const COMPATIBILITY_SOURCE_PLATFORMS = {
+  teradata: { name: 'Teradata', value: 'teradata' },
+  oracle: { name: 'Oracle', value: 'oracle' },
+  netezza: { name: 'Netezza', value: 'netezza' },
+  impala: { name: 'Impala', value: 'impala' },
+  hive: { name: 'Hive', value: 'hive' },
+  db2: { name: 'DB2', value: 'db2' },
+  greenplum: { name: 'Greenplum', value: 'greenplum' },
+  mysql: { name: 'MySQL', value: 'mysql' },
+  postgresql: { name: 'PostgreSQL', value: 'postgresql' },
+  informix: { name: 'Informix', value: 'informix' },
+  sqlserver: { name: 'SQL Server', value: 'sqlserver' },
+  sybase: { name: 'Sybase', value: 'sybase' },
+  access: { name: 'Access', value: 'access' },
+  firebird: { name: 'Firebird', value: 'firebird' },
+  ansisql: { name: 'ANSISQL', value: 'ansisql' },
+  generic: { name: 'Generic', value: 'generic' }
+};
+
+const COMPATIBILITY_TARGET_PLATFORMS = {
+  impala: { name: 'Impala', value: 'impala' },
+  hive: { name: 'Hive', value: 'hive' }
+};
+
+const getDefaultSnippetProperties = function(snippetType) {
+  const properties = {};
+
+  if (snippetType == 'jar' || snippetType == 'py') {
+    properties['driverCores'] = '';
+    properties['executorCores'] = '';
+    properties['numExecutors'] = '';
+    properties['queue'] = '';
+    properties['archives'] = [];
+    properties['files'] = [];
+  }
+
+  if (snippetType == 'java') {
+    properties['archives'] = [];
+    properties['files'] = [];
+    properties['capture_output'] = false;
+  }
+
+  if (snippetType == 'shell') {
+    properties['archives'] = [];
+    properties['files'] = [];
+  }
+
+  if (snippetType == 'mapreduce') {
+    properties['app_jar'] = '';
+    properties['hadoopProperties'] = [];
+    properties['jars'] = [];
+    properties['files'] = [];
+    properties['archives'] = [];
+  }
+
+  if (snippetType == 'spark2') {
+    properties['app_name'] = '';
+    properties['class'] = '';
+    properties['jars'] = [];
+    properties['spark_opts'] = [];
+    properties['spark_arguments'] = [];
+    properties['files'] = [];
+  }
+
+  if (snippetType == 'sqoop1') {
+    properties['files'] = [];
+  }
+
+  if (snippetType == 'jar' || snippetType == 'java') {
+    properties['app_jar'] = '';
+    properties['class'] = '';
+    properties['arguments'] = [];
+  } else if (snippetType == 'distcp') {
+    properties['source_path'] = '';
+    properties['destination_path'] = '';
+  } else if (snippetType == 'shell') {
+    properties['command_path'] = '';
+    properties['arguments'] = [];
+    properties['env_var'] = [];
+    properties['capture_output'] = true;
+  } else if (snippetType == 'py') {
+    properties['py_file'] = '';
+    properties['arguments'] = [];
+  } else if (snippetType == 'hive') {
+    properties['settings'] = [];
+    properties['files'] = [];
+    properties['functions'] = [];
+    properties['arguments'] = [];
+  } else if (snippetType == 'impala') {
+    properties['settings'] = [];
+  } else if (snippetType == 'pig') {
+    properties['parameters'] = [];
+    properties['hadoopProperties'] = [];
+    properties['resources'] = [];
+  }
+
+  return properties;
+};
+
+const ERROR_REGEX = /line ([0-9]+)(\:([0-9]+))?/i;
+
+class Snippet {
+  constructor(vm, notebook, snippet) {
+    const self = this;
+
+    self.id = ko.observable(
+      typeof snippet.id != 'undefined' && snippet.id != null ? snippet.id : hueUtils.UUID()
+    );
+    self.name = ko.observable(
+      typeof snippet.name != 'undefined' && snippet.name != null ? snippet.name : ''
+    );
+    self.type = ko.observable(
+      typeof snippet.type != 'undefined' && snippet.type != null ? snippet.type : 'hive'
+    );
+    self.type.subscribe(newVal => {
+      self.status('ready');
+    });
+
+    self.isBatchable = ko.computed(() => {
+      return (
+        self.type() == 'hive' ||
+        self.type() == 'impala' ||
+        $.grep(vm.availableLanguages, language => {
+          return language.type == self.type() && language.interface == 'oozie';
+        }).length > 0
+      );
+    });
+
+    self.autocompleteSettings = {
+      temporaryOnly: false
+    };
+
+    // Ace stuff
+    self.aceCursorPosition = ko.observable(notebook.isHistory() ? snippet.aceCursorPosition : null);
+
+    let aceEditor = null;
+
+    self.ace = function(newVal) {
+      if (newVal) {
+        aceEditor = newVal;
+        if (!notebook.isPresentationMode()) {
+          aceEditor.focus();
+        }
+      }
+      return aceEditor;
+    };
+    self.errors = ko.observableArray([]);
+
+    self.aceErrorsHolder = ko.observableArray([]);
+    self.aceWarningsHolder = ko.observableArray([]);
+
+    self.aceErrors = ko.pureComputed(() => {
+      return self.showOptimizer() ? self.aceErrorsHolder() : [];
+    });
+    self.aceWarnings = ko.pureComputed(() => {
+      return self.showOptimizer() ? self.aceWarningsHolder() : [];
+    });
+
+    self.availableSnippets = vm.availableSnippets();
+    self.inFocus = ko.observable(false);
+
+    self.inFocus.subscribe(newValue => {
+      if (newValue) {
+        huePubSub.publish('active.snippet.type.changed', self.type());
+      }
+    });
+
+    self.editorMode = vm.editorMode;
+
+    self.getAceMode = function() {
+      return vm.getSnippetViewSettings(self.type()).aceMode;
+    };
+
+    self.dbSelectionVisible = ko.observable(false);
+
+    self.showExecutionAnalysis = ko.observable(false);
+
+    self.isSqlDialect = ko.pureComputed(() => {
+      return vm.getSnippetViewSettings(self.type()).sqlDialect;
+    });
+
+    self.getPlaceHolder = function() {
+      return vm.getSnippetViewSettings(self.type()).placeHolder;
+    };
+
+    self.getApiHelper = function() {
+      return apiHelper;
+    };
+
+    // namespace and compute might be initialized as empty object {}
+    self.namespace = ko.observable(
+      snippet.namespace && snippet.namespace.id ? snippet.namespace : undefined
+    );
+    self.compute = ko.observable(
+      snippet.compute && snippet.compute.id ? snippet.compute : undefined
+    );
+
+    self.whenContextSet = function() {
+      let namespaceSub;
+      const namespaceDeferred = $.Deferred();
+      if (self.namespace()) {
+        namespaceDeferred.resolve(self.namespace());
+      } else {
+        namespaceSub = self.namespace.subscribe(newVal => {
+          if (newVal) {
+            namespaceDeferred.resolve(newVal);
+            namespaceSub.dispose();
+          }
+        });
+      }
+      let computeSub;
+      const computeDeferred = $.Deferred();
+      if (self.compute()) {
+        computeDeferred.resolve(self.compute());
+      } else {
+        computeSub = self.compute.subscribe(newVal => {
+          if (newVal) {
+            computeDeferred.resolve(newVal);
+            computeSub.dispose();
+          }
+        });
+      }
+
+      const result = $.when(namespaceDeferred, computeDeferred);
+
+      result.dispose = function() {
+        if (namespaceSub) {
+          namespaceSub.dispose();
+        }
+        if (computeSub) {
+          computeSub.dispose();
+        }
+        namespaceDeferred.reject();
+        computeDeferred.reject();
+      };
+
+      return result;
+    };
+
+    self.availableDatabases = ko.observableArray();
+    self.database = ko.observable();
+    let previousDatabase = null;
+
+    self.database.subscribe(newValue => {
+      if (newValue !== null) {
+        apiHelper.setInTotalStorage('editor', 'last.selected.database', newValue);
+        if (previousDatabase !== null && previousDatabase !== newValue) {
+          huePubSub.publish('editor.refresh.statement.locations', self);
+        }
+        previousDatabase = newValue;
+      }
+    });
+
+    self.database(
+      typeof snippet.database !== 'undefined' && snippet.database != null ? snippet.database : null
+    );
+
+    // History is currently in Notebook, same with saved queries by snippets, might be better in assist
+    self.currentQueryTab = ko.observable(
+      typeof snippet.currentQueryTab != 'undefined' && snippet.currentQueryTab != null
+        ? snippet.currentQueryTab
+        : 'queryHistory'
+    );
+    self.pinnedContextTabs = ko.observableArray(
+      typeof snippet.pinnedContextTabs != 'undefined' && snippet.pinnedContextTabs != null
+        ? snippet.pinnedContextTabs
+        : []
+    );
+
+    self.removeContextTab = function(context) {
+      if (context.tabId === self.currentQueryTab()) {
+        self.currentQueryTab('queryHistory');
+      }
+      self.pinnedContextTabs.remove(context);
+    };
+
+    self.errorLoadingQueries = ko.observable(false);
+    self.loadingQueries = ko.observable(false);
+
+    self.queriesHasErrors = ko.observable(false);
+    self.queriesCurrentPage = ko.observable(
+      vm.selectedNotebook() && vm.selectedNotebook().snippets().length > 0
+        ? vm
+            .selectedNotebook()
+            .snippets()[0]
+            .queriesCurrentPage()
+        : 1
+    );
+    self.queriesTotalPages = ko.observable(
+      vm.selectedNotebook() && vm.selectedNotebook().snippets().length > 0
+        ? vm
+            .selectedNotebook()
+            .snippets()[0]
+            .queriesTotalPages()
+        : 1
+    );
+    self.queries = ko.observableArray([]);
+
+    self.queriesFilter = ko.observable('');
+    self.queriesFilterVisible = ko.observable(false);
+    self.queriesFilter.extend({ rateLimit: { method: 'notifyWhenChangesStop', timeout: 900 } });
+    self.queriesFilter.subscribe(val => {
+      self.fetchQueries();
+    });
+
+    let lastFetchQueriesRequest = null;
+
+    self.fetchQueries = function() {
+      apiHelper.cancelActiveRequest(lastFetchQueriesRequest);
+
+      const QUERIES_PER_PAGE = 50;
+      lastQueriesPage = self.queriesCurrentPage();
+      self.loadingQueries(true);
+      self.queriesHasErrors(false);
+      lastFetchQueriesRequest = apiHelper.searchDocuments({
+        successCallback: function(result) {
+          self.queriesTotalPages(Math.ceil(result.count / QUERIES_PER_PAGE));
+          self.queries(komapping.fromJS(result.documents)());
+          self.loadingQueries(false);
+          self.queriesHasErrors(false);
+        },
+        errorCallback: function() {
+          self.loadingQueries(false);
+          self.queriesHasErrors(true);
+        },
+        page: self.queriesCurrentPage(),
+        limit: QUERIES_PER_PAGE,
+        type: 'query-' + self.type(),
+        query: self.queriesFilter(),
+        include_trashed: false
+      });
+    };
+
+    let lastQueriesPage = 1;
+    self.currentQueryTab.subscribe(newValue => {
+      huePubSub.publish('redraw.fixed.headers');
+      huePubSub.publish('current.query.tab.switched', newValue);
+      if (
+        newValue === 'savedQueries' &&
+        (self.queries().length === 0 || lastQueriesPage !== self.queriesCurrentPage())
+      ) {
+        self.fetchQueries();
+      }
+    });
+
+    self.prevQueriesPage = function() {
+      if (self.queriesCurrentPage() !== 1) {
+        self.queriesCurrentPage(self.queriesCurrentPage() - 1);
+        self.fetchQueries();
+      }
+    };
+
+    self.nextQueriesPage = function() {
+      if (self.queriesCurrentPage() !== self.queriesTotalPages()) {
+        self.queriesCurrentPage(self.queriesCurrentPage() + 1);
+        self.fetchQueries();
+      }
+    };
+
+    huePubSub.subscribeOnce(
+      'assist.source.set',
+      source => {
+        if (source !== self.type()) {
+          huePubSub.publish('assist.set.source', self.type());
+        }
+      },
+      vm.huePubSubId
+    );
+
+    huePubSub.publish('assist.get.source');
+
+    let ignoreNextAssistDatabaseUpdate = false;
+    self.handleAssistSelection = function(databaseDef) {
+      if (ignoreNextAssistDatabaseUpdate) {
+        ignoreNextAssistDatabaseUpdate = false;
+      } else if (databaseDef.sourceType === self.type()) {
+        if (self.namespace() !== databaseDef.namespace) {
+          self.namespace(databaseDef.namespace);
+        }
+        if (self.database() !== databaseDef.name) {
+          self.database(databaseDef.name);
+        }
+      }
+    };
+
+    if (!self.database()) {
+      huePubSub.publish('assist.get.database.callback', {
+        source: self.type(),
+        callback: function(databaseDef) {
+          self.handleAssistSelection(databaseDef);
+        }
+      });
+    }
+
+    self.statementType = ko.observable(
+      typeof snippet.statementType != 'undefined' && snippet.statementType != null
+        ? snippet.statementType
+        : 'text'
+    );
+    self.statementTypes = ko.observableArray(['text', 'file']); // Maybe computed later for Spark
+    if (!vm.editorMode()) {
+      self.statementTypes.push('document');
+    }
+    self.statementPath = ko.observable(
+      typeof snippet.statementPath != 'undefined' && snippet.statementPath != null
+        ? snippet.statementPath
+        : ''
+    );
+    self.statementPath.subscribe(newVal => {
+      self.getExternalStatement();
+    });
+    self.externalStatementLoaded = ko.observable(false);
+    self.getExternalStatement = function() {
+      self.externalStatementLoaded(false);
+      $.post(
+        '/notebook/api/get_external_statement',
+        {
+          notebook: komapping.toJSON(notebook.getContext()),
+          snippet: komapping.toJSON(self.getContext())
+        },
+        data => {
+          if (data.status == 0) {
+            self.externalStatementLoaded(true);
+            self.statement_raw(data.statement);
+            self.ace().setValue(self.statement_raw(), 1);
+          } else {
+            self._ajaxError(data);
+          }
+        }
+      );
+    };
+    self.associatedDocumentLoading = ko.observable(true);
+    self.associatedDocument = ko.observable();
+    self.associatedDocumentUuid = ko.observable(
+      typeof snippet.associatedDocumentUuid != 'undefined' && snippet.associatedDocumentUuid != null
+        ? snippet.associatedDocumentUuid
+        : null
+    );
+    self.associatedDocumentUuid.subscribe(val => {
+      if (val !== '') {
+        self.getExternalStatement();
+      } else {
+        self.statement_raw('');
+        self.ace().setValue('', 1);
+      }
+    });
+    self.statement_raw = ko.observable(
+      typeof snippet.statement_raw != 'undefined' && snippet.statement_raw != null
+        ? snippet.statement_raw
+        : ''
+    );
+    self.selectedStatement = ko.observable('');
+    self.positionStatement = ko.observable(null);
+    self.lastExecutedStatement = ko.observable(null);
+    self.statementsList = ko.observableArray();
+
+    huePubSub.subscribe(
+      'editor.active.statement.changed',
+      statementDetails => {
+        if (self.ace() && self.ace().container.id === statementDetails.id) {
+          if (statementDetails.activeStatement) {
+            self.positionStatement(statementDetails.activeStatement);
+          } else {
+            self.positionStatement(null);
+          }
+
+          if (statementDetails.activeStatement) {
+            const _statements = [];
+            statementDetails.precedingStatements.forEach(statement => {
+              _statements.push(statement.statement);
+            });
+            _statements.push(statementDetails.activeStatement.statement);
+            statementDetails.followingStatements.forEach(statement => {
+              _statements.push(statement.statement);
+            });
+            self.statementsList(_statements); // Or fetch on demand via editor.refresh.statement.locations and remove observableArray?
+          } else {
+            self.statementsList([]);
+          }
+          if (!notebook.isPresentationModeInitialized()) {
+            if (notebook.isPresentationModeDefault()) {
+              // When switching to presentation mode, the snippet in non presentation mode cannot get status notification.
+              // On initiailization, status is set to loading and does not get updated, because we moved to presentation mode.
+              self.status('ready');
+            }
+            // Changing to presentation mode requires statementsList to be initialized. statementsList is initialized asynchronously.
+            // When presentation mode is default, we cannot change before statementsList has been calculated.
+            // Cleaner implementation would be to make toggleEditorMode statementsList asynchronous
+            // However this is currently impossible due to delete _notebook.presentationSnippets()[key];
+            notebook.isPresentationModeInitialized(true);
+            notebook.isPresentationMode(notebook.isPresentationModeDefault());
+          }
+        }
+      },
+      vm.huePubSubId
+    );
+
+    self.aceSize = ko.observable(
+      typeof snippet.aceSize != 'undefined' && snippet.aceSize != null ? snippet.aceSize : 100
+    );
+    // self.statement_raw.extend({ rateLimit: 150 }); // Should prevent lag from typing but currently send the old query when using the key shortcut
+    self.status = ko.observable(
+      typeof snippet.status != 'undefined' && snippet.status != null ? snippet.status : 'loading'
+    );
+    self.statusForButtons = ko.observable('executed');
+
+    self.properties = ko.observable(
+      komapping.fromJS(
+        typeof snippet.properties != 'undefined' && snippet.properties != null
+          ? snippet.properties
+          : getDefaultSnippetProperties(self.type())
+      )
+    );
+    self.hasProperties = ko.computed(() => {
+      return Object.keys(komapping.toJS(self.properties())).length > 0;
+    });
+
+    self.viewSettings = ko.computed(() => {
+      return vm.getSnippetViewSettings(self.type());
+    });
+
+    const previousProperties = {};
+    self.type.subscribe(
+      oldValue => {
+        previousProperties[oldValue] = self.properties();
+      },
+      null,
+      'beforeChange'
+    );
+
+    self.type.subscribe(newValue => {
+      if (typeof previousProperties[newValue] != 'undefined') {
+        self.properties(previousProperties[newValue]);
+      } else {
+        self.properties(komapping.fromJS(getDefaultSnippetProperties(newValue)));
+      }
+      self.result.clear();
+      window.setTimeout(() => {
+        if (self.ace() !== null) {
+          self.ace().focus();
+        }
+      }, 100);
+    });
+    if (snippet.variables) {
+      snippet.variables.forEach(variable => {
+        variable.meta = (typeof variable.defaultValue === 'object' && variable.defaultValue) || {
+          type: 'text',
+          placeholder: ''
+        };
+        variable.value = variable.value || '';
+        variable.type = variable.type || 'text';
+        variable.sample = [];
+        variable.sampleUser = variable.sampleUser || [];
+        variable.path = variable.path || '';
+        variable.step = '';
+        delete variable.defaultValue;
+      });
+    }
+    self.variables = komapping.fromJS(
+      typeof snippet.variables != 'undefined' && snippet.variables != null ? snippet.variables : []
+    );
+    self.variables.subscribe(newValue => {
+      $(document).trigger('updateResultHeaders', self);
+    });
+    self.hasCurlyBracketParameters = ko.computed(() => {
+      return self.type() != 'pig';
+    });
+    self.getPigParameters = function() {
+      const params = {};
+      const variables = self.statement_raw().match(/([^\\]|^)\$[^\d'"](\w*)/g);
+      const declares = self.statement_raw().match(/%declare +([^ ])+/gi);
+      const defaults = self.statement_raw().match(/%default +([^;])+/gi);
+      const macro_defines = self.statement_raw().match(/define [^ ]+ *\(([^\)]*)\)/gi); // no multiline
+      const macro_returns = self.statement_raw().match(/returns +([^\{]*)/gi); // no multiline
+
+      if (variables) {
+        $.each(variables, (index, param) => {
+          const p = param.substring(param.indexOf('$') + 1);
+          params[p] = '';
+        });
+      }
+      if (declares) {
+        $.each(declares, (index, param) => {
+          param = param.match(/(\w+)/g);
+          if (param && param.length >= 2) {
+            delete params[param[1]];
+          }
+        });
+      }
+      if (defaults) {
+        $.each(defaults, (index, param) => {
+          const line = param.match(/(\w+)/g);
+          if (line && line.length >= 2) {
+            const name = line[1];
+            params[name] = param.substring(param.indexOf(name) + name.length + 1);
+          }
+        });
+      }
+      if (macro_defines) {
+        $.each(macro_defines, (index, params_line) => {
+          const param_line = params_line.match(/(\w+)/g);
+          if (param_line && param_line.length > 2) {
+            $.each(param_line, (index, param) => {
+              if (index >= 2) {
+                // Skips define NAME
+                delete params[param];
+              }
+            });
+          }
+        });
+      }
+      if (macro_returns) {
+        $.each(macro_returns, (index, params_line) => {
+          const param_line = params_line.match(/(\w+)/g);
+          if (param_line) {
+            $.each(param_line, (index, param) => {
+              if (index >= 1) {
+                // Skip returns
+                delete params[param];
+              }
+            });
+          }
+        });
+      }
+
+      return params;
+    };
+    self.variableNames = ko.computed(() => {
+      let match,
+        matches = {},
+        matchList;
+      if (self.type() == 'pig') {
+        matches = self.getPigParameters();
+      } else {
+        const re = /(?:^|\W)\${(\w*)\=?([^{}]*)}/g;
+        const reComment = /(^\s*--.*)|(\/\*[\s\S]*?\*\/)/gm;
+        const reList = /(?!\s*$)\s*(?:(?:([^,|()\\]*)\(\s*([^,|()\\]*)\)(?:\\[\S\s][^,|()\\]*)?)|([^,|\\]*(?:\\[\S\s][^,|\\]*)*))\s*(?:,|\||$)/g;
+        const statement = self.statement_raw();
+        let matchComment = reComment.exec(statement);
+        // if re is n & reComment is m
+        // finding variables is O(n+m)
+        while ((match = re.exec(statement))) {
+          while (matchComment && match.index > matchComment.index + matchComment[0].length) {
+            // Comments before our match
+            matchComment = reComment.exec(statement);
+          }
+          const isWithinComment = matchComment && match.index >= matchComment.index;
+          if (isWithinComment) {
+            continue;
+          }
+
+          // If 1 match, text value
+          // If multiple matches, list value
+          const value = { type: 'text', placeholder: '' };
+          while ((matchList = reList.exec(match[2]))) {
+            const option = {
+              text: matchList[2] || matchList[3],
+              value: matchList[3] || matchList[1]
+            };
+            option.text = option.text && option.text.trim();
+            option.value =
+              option.value &&
+              option.value
+                .trim()
+                .replace(',', ',')
+                .replace('(', '(')
+                .replace(')', ')');
+
+            if (value.placeholder || matchList[2]) {
+              if (!value.options) {
+                value.options = [];
+                value.type = 'select';
+              }
+              value.options.push(option);
+            }
+            if (!value.placeholder) {
+              value.placeholder = option.value;
+            }
+          }
+          const isPlaceholderInOptions =
+            !value.options ||
+            value.options.some(current => {
+              return current.value == value.placeholder;
+            });
+          if (!isPlaceholderInOptions) {
+            value.options.unshift({ text: value.placeholder, value: value.placeholder });
+          }
+          matches[match[1]] = matches[match[1]] || value;
+        }
+      }
+      return $.map(matches, (match, key) => {
+        const isMatchObject = typeof matches[key] === 'object';
+        const meta = isMatchObject ? matches[key] : { type: 'text', placeholder: matches[key] };
+        return { name: key, meta: meta };
+      });
+    });
+    self.variableValues = {};
+    self.variableNames.extend({ rateLimit: 150 });
+    self.variableNames.subscribe(newVal => {
+      const variablesLength = self.variables().length;
+      const diffLengthVariables = variablesLength - newVal.length;
+      const needsMore = diffLengthVariables < 0;
+      const needsLess = diffLengthVariables > 0;
+      self.variableValues = self.variables().reduce((variableValues, variable) => {
+        if (!variableValues[variable.name()]) {
+          variableValues[variable.name()] = { sampleUser: [] };
+        }
+        variableValues[variable.name()].value = variable.value();
+        variableValues[variable.name()].sampleUser = variable.sampleUser();
+        variableValues[variable.name()].catalogEntry = variable.catalogEntry;
+        variableValues[variable.name()].path = variable.path();
+        variableValues[variable.name()].type = variable.type();
+        return variableValues;
+      }, self.variableValues);
+      if (needsMore) {
+        for (let i = 0, length = Math.abs(diffLengthVariables); i < length; i++) {
+          self.variables.push(
+            komapping.fromJS({
+              name: '',
+              value: '',
+              meta: { type: 'text', placeholder: '', options: [] },
+              sample: [],
+              sampleUser: [],
+              type: 'text',
+              step: '',
+              path: ''
+            })
+          );
+        }
+      } else if (needsLess) {
+        self.variables.splice(self.variables().length - diffLengthVariables, diffLengthVariables);
+      }
+      newVal.forEach((item, index) => {
+        const variable = self.variables()[index];
+        variable.name(item.name);
+        setTimeout(() => {
+          variable.value(
+            self.variableValues[item.name]
+              ? self.variableValues[item.name].value
+              : (!needsMore && variable.value()) || ''
+          );
+        }, 0);
+        variable.meta = komapping.fromJS(item.meta, {}, variable.meta);
+        variable.sample(
+          variable.meta.options
+            ? variable.meta.options().concat(variable.sampleUser())
+            : variable.sampleUser()
+        );
+        variable.sampleUser(
+          self.variableValues[item.name] ? self.variableValues[item.name].sampleUser : []
+        );
+        variable.type(
+          self.variableValues[item.name] ? self.variableValues[item.name].type || 'text' : 'text'
+        );
+        variable.path(
+          self.variableValues[item.name] ? self.variableValues[item.name].path || '' : ''
+        );
+        variable.catalogEntry =
+          self.variableValues[item.name] && self.variableValues[item.name].catalogEntry;
+      });
+    });
+
+    const activeSourcePromises = [];
+    huePubSub.subscribe('ace.sql.location.worker.message', e => {
+      while (activeSourcePromises.length) {
+        const promise = activeSourcePromises.pop();
+        if (promise.cancel) {
+          promise.cancel();
+        }
+      }
+      const oLocations = e.data.locations
+        .filter(location => {
+          return location.type === 'variable' && location.colRef;
+        })
+        .reduce((variables, location) => {
+          const re = /\${(\w*)\=?([^{}]*)}/g;
+          const name = re.exec(location.value)[1];
+          variables[name] = location;
+          return variables;
+        }, {});
+      const updateVariableType = function(variable, sourceMeta) {
+        let type;
+        if (sourceMeta && sourceMeta.type) {
+          type = sourceMeta.type.toLowerCase();
+        } else {
+          type = 'string';
+        }
+        const variablesValues = {};
+        const value = variable.value();
+        switch (type) {
+          case 'timestamp':
+            variablesValues.type = 'datetime-local';
+            variablesValues.step = '1';
+            variablesValues.value =
+              (value && moment.utc(value).format('YYYY-MM-DD HH:mm:ss.S')) ||
+              moment(Date.now()).format('YYYY-MM-DD 00:00:00.0');
+            break;
+          case 'decimal':
+          case 'double':
+          case 'float':
+            variablesValues.type = 'number';
+            variablesValues.step = 'any';
+            break;
+          case 'int':
+          case 'smallint':
+          case 'tinyint':
+          case 'bigint':
+            variablesValues.type = 'number';
+            variablesValues.step = '1';
+            break;
+          case 'date':
+            variablesValues.type = 'date';
+            variablesValues.step = '';
+            variablesValues.value =
+              (value && moment.utc(value).format('YYYY-MM-DD')) ||
+              moment(Date.now()).format('YYYY-MM-DD');
+            break;
+          case 'boolean':
+            variablesValues.type = 'checkbox';
+            variablesValues.step = '';
+            break;
+          default:
+            variablesValues.type = 'text';
+            variablesValues.step = '';
+        }
+        if (variablesValues.value) {
+          setTimeout(() => {
+            variable.value(variablesValues.value);
+          }, 0);
+        }
+        variable.type(variablesValues.type);
+        variable.step(variablesValues.step);
+      };
+      self.variables().forEach(variable => {
+        if (oLocations[variable.name()]) {
+          activeSourcePromises.push(
+            oLocations[variable.name()].resolveCatalogEntry({ cancellable: true }).done(entry => {
+              variable.path(entry.path.join('.'));
+              variable.catalogEntry = entry;
+
+              activeSourcePromises.push(
+                entry
+                  .getSourceMeta({
+                    silenceErrors: true,
+                    cancellable: true
+                  })
+                  .then(updateVariableType.bind(self, variable))
+              );
+            })
+          );
+        } else {
+          updateVariableType(variable, {
+            type: 'text'
+          });
+        }
+      });
+    });
+    self.statement = ko.computed(() => {
+      let statement = self.isSqlDialect()
+        ? self.selectedStatement()
+          ? self.selectedStatement()
+          : self.positionStatement() !== null
+          ? self.positionStatement().statement
+          : self.statement_raw()
+        : self.statement_raw();
+      const variables = self.variables().reduce((variables, variable) => {
+        variables[variable.name()] = variable;
+        return variables;
+      }, {});
+      if (self.variables().length) {
+        const variablesString = self
+          .variables()
+          .map(variable => {
+            return variable.name();
+          })
+          .join('|');
+        statement = statement.replace(
+          RegExp(
+            '([^\\\\])?\\$' +
+              (self.hasCurlyBracketParameters() ? '{(' : '(') +
+              variablesString +
+              ')(=[^}]*)?' +
+              (self.hasCurlyBracketParameters() ? '}' : ''),
+            'g'
+          ),
+          (match, p1, p2) => {
+            const variable = variables[p2];
+            const pad =
+              variable.type() == 'datetime-local' && variable.value().length == 16 ? ':00' : ''; // Chrome drops the seconds from the timestamp when it's at 0 second.
+            const value = variable.value();
+            return (
+              p1 +
+              (value !== undefined && value !== null
+                ? value + pad
+                : variable.meta.placeholder && variable.meta.placeholder())
+            );
+          }
+        );
+      }
+      return statement;
+    });
+
+    self.result = new Result(snippet, snippet.result);
+    if (!self.result.hasSomeResults()) {
+      self.currentQueryTab('queryHistory');
+    }
+    self.showGrid = ko.observable(
+      typeof snippet.showGrid != 'undefined' && snippet.showGrid != null ? snippet.showGrid : true
+    );
+    self.showChart = ko.observable(
+      typeof snippet.showChart != 'undefined' && snippet.showChart != null
+        ? snippet.showChart
+        : false
+    );
+    let defaultShowLogs = true;
+    if (vm.editorMode() && $.totalStorage('hue.editor.showLogs')) {
+      defaultShowLogs = $.totalStorage('hue.editor.showLogs');
+    }
+    self.showLogs = ko.observable(
+      typeof snippet.showLogs !== 'undefined' && snippet.showLogs != null
+        ? snippet.showLogs
+        : defaultShowLogs
+    );
+    self.progress = ko.observable(
+      typeof snippet.progress !== 'undefined' && snippet.progress != null ? snippet.progress : 0
+    );
+    self.jobs = ko.observableArray(
+      typeof snippet.jobs !== 'undefined' && snippet.jobs != null ? snippet.jobs : []
+    );
+
+    self.executeNextTimeout = -1;
+    const refreshTimeouts = {};
+    self.onDdlExecute = function() {
+      if (self.result.handle() && self.result.handle().has_more_statements) {
+        window.clearTimeout(self.executeNextTimeout);
+        self.executeNextTimeout = setTimeout(() => {
+          self.execute(true); // Execute next, need to wait as we disabled fast click
+        }, 1000);
+      }
+      if (
+        self.lastExecutedStatement() &&
+        /CREATE|DROP/i.test(self.lastExecutedStatement().firstToken)
+      ) {
+        let match = self
+          .statement()
+          .match(
+            /(?:CREATE|DROP)\s+TABLE\s+(?:IF\s+(?:NOT\s+)?EXISTS\s+)?(?:`([^`]+)`|([^;\s]+))\..*/i
+          );
+        const path = [];
+        if (match) {
+          path.push(match[1] || match[2]); // group 1 backticked db name, group 2 regular db name
+        } else {
+          match = self
+            .statement()
+            .match(
+              /(?:CREATE|DROP)\s+(?:DATABASE|SCHEMA)\s+(?:IF\s+(?:NOT\s+)?EXISTS\s+)?(?:`([^`]+)`|([^;\s]+))/i
+            );
+          if (match) {
+            path.push(match[1] || match[2]); // group 1 backticked db name, group 2 regular db name
+          } else if (self.database()) {
+            path.push(self.database());
+          }
+        }
+
+        if (path.length) {
+          window.clearTimeout(refreshTimeouts[path.join('.')]);
+          refreshTimeouts[path.join('.')] = window.setTimeout(() => {
+            ignoreNextAssistDatabaseUpdate = true;
+            dataCatalog
+              .getEntry({
+                sourceType: self.type(),
+                namespace: self.namespace(),
+                compute: self.compute(),
+                path: path
+              })
+              .done(entry => {
+                entry.clearCache({ invalidate: 'invalidate', cascade: true, silenceErrors: true });
+              });
+          }, 5000);
+        }
+      }
+    };
+
+    self.progress.subscribe(val => {
+      $(document).trigger('progress', { data: val, snippet: self });
+    });
+
+    self.showGrid.subscribe(val => {
+      if (val) {
+        self.showChart(false);
+        huePubSub.publish('editor.grid.shown', self);
+      }
+    });
+
+    function prepopulateChart() {
+      const type = self.chartType();
+      hueAnalytics.log('notebook', 'chart/' + type);
+
+      if (type === ko.HUE_CHARTS.TYPES.MAP && self.result.cleanedNumericMeta().length >= 2) {
+        if (self.chartX() === null || typeof self.chartX() === 'undefined') {
+          let name = self.result.cleanedNumericMeta()[0].name;
+          self.result.cleanedNumericMeta().forEach(fld => {
+            if (
+              fld.name.toLowerCase().indexOf('lat') > -1 ||
+              fld.name.toLowerCase().indexOf('ltd') > -1
+            ) {
+              name = fld.name;
+            }
+          });
+          self.chartX(name);
+        }
+        if (self.chartYSingle() === null || typeof self.chartYSingle() === 'undefined') {
+          let name = self.result.cleanedNumericMeta()[1].name;
+          self.result.cleanedNumericMeta().forEach(fld => {
+            if (
+              fld.name.toLowerCase().indexOf('lon') > -1 ||
+              fld.name.toLowerCase().indexOf('lng') > -1
+            ) {
+              name = fld.name;
+            }
+          });
+          self.chartYSingle(name);
+        }
+        return;
+      }
+
+      if (
+        (self.chartX() === null || typeof self.chartX() === 'undefined') &&
+        (type == ko.HUE_CHARTS.TYPES.BARCHART ||
+          type == ko.HUE_CHARTS.TYPES.PIECHART ||
+          type == ko.HUE_CHARTS.TYPES.GRADIENTMAP) &&
+        self.result.cleanedStringMeta().length >= 1
+      ) {
+        self.chartX(self.result.cleanedStringMeta()[0].name);
+      }
+
+      if (self.result.cleanedNumericMeta().length > 0) {
+        if (
+          self.chartYMulti().length === 0 &&
+          (type === ko.HUE_CHARTS.TYPES.BARCHART || type === ko.HUE_CHARTS.TYPES.LINECHART)
+        ) {
+          self.chartYMulti.push(
+            self.result.cleanedNumericMeta()[
+              Math.min(self.result.cleanedNumericMeta().length - 1, 1)
+            ].name
+          );
+        } else if (
+          (self.chartYSingle() === null || typeof self.chartYSingle() === 'undefined') &&
+          (type === ko.HUE_CHARTS.TYPES.PIECHART ||
+            type === ko.HUE_CHARTS.TYPES.MAP ||
+            type === ko.HUE_CHARTS.TYPES.GRADIENTMAP ||
+            type === ko.HUE_CHARTS.TYPES.SCATTERCHART ||
+            (type === ko.HUE_CHARTS.TYPES.BARCHART && self.chartXPivot() !== null))
+        ) {
+          if (self.chartYMulti().length === 0) {
+            self.chartYSingle(
+              self.result.cleanedNumericMeta()[
+                Math.min(self.result.cleanedNumericMeta().length - 1, 1)
+              ].name
+            );
+          } else {
+            self.chartYSingle(self.chartYMulti()[0]);
+          }
+        }
+      }
+    }
+
+    self.showChart.subscribe(val => {
+      if (val) {
+        self.showGrid(false);
+        self.isResultSettingsVisible(true);
+        $(document).trigger('forceChartDraw', self);
+        huePubSub.publish('editor.chart.shown', self);
+        prepopulateChart();
+      }
+    });
+    self.showLogs.subscribe(val => {
+      huePubSub.publish('redraw.fixed.headers');
+      if (val) {
+        self.getLogs();
+      }
+      if (vm.editorMode()) {
+        $.totalStorage('hue.editor.showLogs', val);
+      }
+    });
+
+    self.isLoading = ko.computed(() => {
+      return self.status() == 'loading';
+    });
+
+    self.resultsKlass = ko.computed(() => {
+      return 'results ' + self.type();
+    });
+
+    self.errorsKlass = ko.computed(() => {
+      return self.resultsKlass() + ' alert alert-error';
+    });
+
+    self.is_redacted = ko.observable(
+      typeof snippet.is_redacted != 'undefined' && snippet.is_redacted != null
+        ? snippet.is_redacted
+        : false
+    );
+
+    self.chartType = ko.observable(
+      typeof snippet.chartType != 'undefined' && snippet.chartType != null
+        ? snippet.chartType
+        : ko.HUE_CHARTS.TYPES.BARCHART
+    );
+    self.chartType.subscribe(prepopulateChart);
+    self.chartSorting = ko.observable(
+      typeof snippet.chartSorting != 'undefined' && snippet.chartSorting != null
+        ? snippet.chartSorting
+        : 'none'
+    );
+    self.chartScatterGroup = ko.observable(
+      typeof snippet.chartScatterGroup != 'undefined' && snippet.chartScatterGroup != null
+        ? snippet.chartScatterGroup
+        : null
+    );
+    self.chartScatterSize = ko.observable(
+      typeof snippet.chartScatterSize != 'undefined' && snippet.chartScatterSize != null
+        ? snippet.chartScatterSize
+        : null
+    );
+    self.chartScope = ko.observable(
+      typeof snippet.chartScope != 'undefined' && snippet.chartScope != null
+        ? snippet.chartScope
+        : 'world'
+    );
+    self.chartTimelineType = ko.observable(
+      typeof snippet.chartTimelineType != 'undefined' && snippet.chartTimelineType != null
+        ? snippet.chartTimelineType
+        : 'bar'
+    );
+    self.chartLimits = ko.observableArray([5, 10, 25, 50, 100]);
+    self.chartLimit = ko.observable(
+      typeof snippet.chartLimit != 'undefined' && snippet.chartLimit != null
+        ? snippet.chartLimit
+        : null
+    );
+    self.chartLimit.extend({ notify: 'always' });
+    self.chartX = ko.observable(
+      typeof snippet.chartX != 'undefined' && snippet.chartX != null ? snippet.chartX : null
+    );
+    self.chartX.extend({ notify: 'always' });
+    self.chartXPivot = ko.observable(
+      typeof snippet.chartXPivot != 'undefined' && snippet.chartXPivot != null
+        ? snippet.chartXPivot
+        : null
+    );
+    self.chartXPivot.extend({ notify: 'always' });
+    self.chartXPivot.subscribe(prepopulateChart);
+    self.chartYSingle = ko.observable(
+      typeof snippet.chartYSingle != 'undefined' && snippet.chartYSingle != null
+        ? snippet.chartYSingle
+        : null
+    );
+    self.chartYMulti = ko.observableArray(
+      typeof snippet.chartYMulti != 'undefined' && snippet.chartYMulti != null
+        ? snippet.chartYMulti
+        : []
+    );
+    self.chartData = ko.observableArray(
+      typeof snippet.chartData != 'undefined' && snippet.chartData != null ? snippet.chartData : []
+    );
+    self.chartMapType = ko.observable(
+      typeof snippet.chartMapType != 'undefined' && snippet.chartMapType != null
+        ? snippet.chartMapType
+        : 'marker'
+    );
+    self.chartMapLabel = ko.observable(
+      typeof snippet.chartMapLabel != 'undefined' && snippet.chartMapLabel != null
+        ? snippet.chartMapLabel
+        : null
+    );
+    self.chartMapHeat = ko.observable(
+      typeof snippet.chartMapHeat != 'undefined' && snippet.chartMapHeat != null
+        ? snippet.chartMapHeat
+        : null
+    );
+    self.hideStacked = ko.computed(() => {
+      return self.chartYMulti().length <= 1;
+    });
+
+    self.hasDataForChart = ko.computed(() => {
+      if (
+        self.chartType() == ko.HUE_CHARTS.TYPES.BARCHART ||
+        self.chartType() == ko.HUE_CHARTS.TYPES.LINECHART ||
+        self.chartType() == ko.HUE_CHARTS.TYPES.TIMELINECHART
+      ) {
+        return (
+          typeof self.chartX() != 'undefined' &&
+          self.chartX() != null &&
+          self.chartYMulti().length > 0
+        );
+      }
+      return (
+        typeof self.chartX() != 'undefined' &&
+        self.chartX() != null &&
+        typeof self.chartYSingle() != 'undefined' &&
+        self.chartYSingle() != null
+      );
+    });
+
+    self.hasDataForChart.subscribe(newValue => {
+      self.chartX.notifySubscribers();
+      self.chartX.valueHasMutated();
+    });
+
+    self.chartType.subscribe(val => {
+      $(document).trigger('forceChartDraw', self);
+    });
+
+    self.previousChartOptions = {};
+
+    function guessMetaField(field) {
+      let _fld = null;
+      if (field) {
+        if (self.result.cleanedMeta().length > 0) {
+          self.result.cleanedMeta().forEach(fld => {
+            if (
+              fld.name.toLowerCase() === field.toLowerCase() ||
+              field.toLowerCase() === fld.name.toLowerCase()
+            ) {
+              _fld = fld.name;
+            }
+          });
+        }
+      }
+      return _fld;
+    }
+
+    function guessMetaFields(fields) {
+      const _fields = [];
+      if (fields) {
+        fields.forEach(fld => {
+          const _field = guessMetaField(fld);
+          if (_field) {
+            _fields.push(_field);
+          }
+        });
+      }
+      return _fields;
+    }
+
+    self.result.meta.subscribe(newValue => {
+      self.chartLimit(self.previousChartOptions.chartLimit);
+      self.chartX(guessMetaField(self.previousChartOptions.chartX));
+      self.chartXPivot(self.previousChartOptions.chartXPivot);
+      self.chartYSingle(guessMetaField(self.previousChartOptions.chartYSingle));
+      self.chartMapType(self.previousChartOptions.chartMapType);
+      self.chartMapLabel(guessMetaField(self.previousChartOptions.chartMapLabel));
+      self.chartMapHeat(self.previousChartOptions.chartMapHeat);
+      self.chartYMulti(guessMetaFields(self.previousChartOptions.chartYMulti) || []);
+      self.chartSorting(self.previousChartOptions.chartSorting);
+      self.chartScatterGroup(self.previousChartOptions.chartScatterGroup);
+      self.chartScatterSize(self.previousChartOptions.chartScatterSize);
+      self.chartScope(self.previousChartOptions.chartScope);
+      self.chartTimelineType(self.previousChartOptions.chartTimelineType);
+    });
+
+    self.isResultSettingsVisible = ko.observable(
+      typeof snippet.isResultSettingsVisible != 'undefined' &&
+        snippet.isResultSettingsVisible != null
+        ? snippet.isResultSettingsVisible
+        : false
+    );
+    self.toggleResultSettings = function() {
+      self.isResultSettingsVisible(!self.isResultSettingsVisible());
+    };
+    self.isResultSettingsVisible.subscribe(() => {
+      $(document).trigger('toggleResultSettings', self);
+    });
+
+    self.settingsVisible = ko.observable(
+      typeof snippet.settingsVisible != 'undefined' && snippet.settingsVisible != null
+        ? snippet.settingsVisible
+        : false
+    );
+    self.saveResultsModalVisible = ko.observable(false);
+
+    self.checkStatusTimeout = null;
+
+    self.getContext = function() {
+      return {
+        id: self.id,
+        type: self.type,
+        status: self.status,
+        statementType: self.statementType,
+        statement: self.statement,
+        aceCursorPosition: self.aceCursorPosition,
+        statementPath: self.statementPath,
+        associatedDocumentUuid: self.associatedDocumentUuid,
+        properties: self.properties,
+        result: self.result.getContext(),
+        database: self.database,
+        compute: self.compute(),
+        wasBatchExecuted: self.wasBatchExecuted()
+      };
+    };
+
+    self.complexity = ko.observable();
+    self.hasComplexity = ko.pureComputed(() => {
+      return self.complexity() && Object.keys(self.complexity()).length > 0;
+    });
+    self.hasRisks = ko.pureComputed(() => {
+      return (
+        self.hasComplexity() && self.complexity()['hints'] && self.complexity()['hints'].length > 0
+      );
+    });
+    self.topRisk = ko.pureComputed(() => {
+      if (self.hasRisks()) {
+        return self.complexity()['hints'][0];
+      } else {
+        return null;
+      }
+    });
+
+    self.suggestion = ko.observable('');
+    self.hasSuggestion = ko.observable(null);
+
+    self.compatibilityCheckRunning = ko.observable(false);
+
+    self.compatibilitySourcePlatforms = [];
+    Object.keys(COMPATIBILITY_SOURCE_PLATFORMS).forEach(key => {
+      self.compatibilitySourcePlatforms.push(COMPATIBILITY_SOURCE_PLATFORMS[key]);
+    });
+
+    self.compatibilitySourcePlatform = ko.observable(COMPATIBILITY_SOURCE_PLATFORMS[self.type()]);
+    self.compatibilitySourcePlatform.subscribe(newValue => {
+      if (newValue && newValue.value !== self.type()) {
+        self.hasSuggestion(null);
+        self.compatibilityTargetPlatform(COMPATIBILITY_TARGET_PLATFORMS[self.type()]);
+        self.queryCompatibility();
+      }
+    });
+
+    self.compatibilityTargetPlatforms = [];
+    Object.keys(COMPATIBILITY_TARGET_PLATFORMS).forEach(key => {
+      self.compatibilityTargetPlatforms.push(COMPATIBILITY_TARGET_PLATFORMS[key]);
+    });
+    self.compatibilityTargetPlatform = ko.observable(COMPATIBILITY_TARGET_PLATFORMS[self.type()]);
+
+    self.showOptimizer = ko.observable(
+      apiHelper.getFromTotalStorage('editor', 'show.optimizer', false)
+    );
+    self.showOptimizer.subscribe(newValue => {
+      if (newValue !== null) {
+        apiHelper.setInTotalStorage('editor', 'show.optimizer', newValue);
+      }
+    });
+
+    if (HAS_OPTIMIZER && !vm.isNotificationManager()) {
+      let lastComplexityRequest;
+      let lastCheckedComplexityStatement;
+      const knownResponses = [];
+
+      self.delayedStatement = ko
+        .pureComputed(self.statement)
+        .extend({ rateLimit: { method: 'notifyWhenChangesStop', timeout: 2000 } });
+
+      const handleRiskResponse = function(data) {
+        if (data.status == 0) {
+          self.hasSuggestion('');
+          self.complexity(data.query_complexity);
+        } else {
+          self.hasSuggestion('error');
+          self.complexity({ hints: [] });
+        }
+        huePubSub.publish('editor.active.risks', {
+          editor: self.ace(),
+          risks: self.complexity() || {}
+        });
+        lastCheckedComplexityStatement = self.statement();
+      };
+
+      const clearActiveRisks = function() {
+        if (self.hasSuggestion() !== null && typeof self.hasSuggestion() !== 'undefined') {
+          self.hasSuggestion(null);
+        }
+
+        if (self.suggestion() !== '') {
+          self.suggestion('');
+        }
+
+        if (self.complexity() !== {}) {
+          self.complexity(undefined);
+          huePubSub.publish('editor.active.risks', {
+            editor: self.ace(),
+            risks: {}
+          });
+        }
+      };
+
+      self.positionStatement.subscribe(newStatement => {
+        if (newStatement) {
+          const hash = newStatement.statement.hashCode();
+          const unknownResponse = knownResponses.every(knownResponse => {
+            if (knownResponse.hash === hash) {
+              handleRiskResponse(knownResponse.data);
+              return false;
+            }
+            return true;
+          });
+          if (unknownResponse) {
+            clearActiveRisks();
+          }
+        }
+      });
+
+      self.checkComplexity = function() {
+        if (!self.inFocus() || lastCheckedComplexityStatement === self.statement()) {
+          return;
+        }
+
+        // The syntaxError property is only set if the syntax checker is active and has found an
+        // error, see AceLocationHandler.
+        if (self.positionStatement() && self.positionStatement().syntaxError) {
+          return;
+        }
+
+        apiHelper.cancelActiveRequest(lastComplexityRequest);
+
+        hueAnalytics.log('notebook', 'get_query_risk');
+        clearActiveRisks();
+
+        const changeSubscription = self.statement.subscribe(() => {
+          changeSubscription.dispose();
+          apiHelper.cancelActiveRequest(lastComplexityRequest);
+        });
+
+        const hash = self.statement().hashCode();
+
+        const unknownResponse = knownResponses.every(knownResponse => {
+          if (knownResponse.hash === hash) {
+            handleRiskResponse(knownResponse.data);
+            return false;
+          }
+          return true;
+        });
+        if (unknownResponse) {
+          lastComplexityRequest = $.ajax({
+            type: 'POST',
+            url: '/notebook/api/optimizer/statement/risk',
+            timeout: 30000, // 30 seconds
+            data: {
+              notebook: komapping.toJSON(notebook.getContext()),
+              snippet: komapping.toJSON(self.getContext())
+            },
+            success: function(data) {
+              knownResponses.unshift({
+                hash: hash,
+                data: data
+              });
+              if (knownResponses.length > 50) {
+                knownResponses.pop();
+              }
+              handleRiskResponse(data);
+            },
+            always: function(data) {
+              changeSubscription.dispose();
+            }
+          });
+        }
+      };
+
+      if (self.type() === 'hive' || self.type() === 'impala') {
+        if (self.statement_raw()) {
+          window.setTimeout(() => {
+            self.checkComplexity();
+          }, 2000);
+        }
+        self.delayedStatement.subscribe(() => {
+          self.checkComplexity();
+        });
+      }
+    }
+
+    self._ajaxError = function(data, callback) {
+      if (data.status == -2) {
+        // Session expired
+        const existingSession = notebook.getSession(self.type());
+        if (existingSession) {
+          notebook.restartSession(existingSession, callback);
+        } else {
+          notebook.createSession(new Session(vm, { type: self.type() }), callback);
+        }
+      } else if (data.status == -3) {
+        // Statement expired
+        self.status('expired');
+        if (data.message) {
+          self.errors.push({ message: data.message, help: null, line: null, col: null });
+          huePubSub.publish('editor.snippet.result.normal', self);
+        }
+      } else if (data.status == -4) {
+        // Operation timed out
+        notebook.retryModalCancel = function() {
+          self.status('failed');
+          huePubSub.publish('hide.retry.modal');
+        };
+        notebook.retryModalConfirm = function() {
+          if (callback) {
+            callback();
+          }
+          huePubSub.publish('hide.retry.modal');
+        };
+        huePubSub.publish('show.retry.modal');
+      } else if (data.status == 401) {
+        // Auth required
+        self.status('expired');
+        $(document).trigger('showAuthModal', { type: self.type(), callback: self.execute });
+      } else if (data.status == 1 || data.status == -1) {
+        self.status('failed');
+        const match = ERROR_REGEX.exec(data.message);
+        if (match) {
+          let errorLine = parseInt(match[1]);
+          let errorCol;
+          if (typeof match[3] !== 'undefined') {
+            errorCol = parseInt(match[3]);
+          }
+          if (self.positionStatement()) {
+            if (errorCol && errorLine === 1) {
+              errorCol += self.positionStatement().location.first_column;
+            }
+            errorLine += self.positionStatement().location.first_line - 1;
+          }
+
+          self.errors.push({
+            message: data.message.replace(
+              match[0],
+              'line ' + errorLine + (errorCol !== null ? ':' + errorCol : '')
+            ),
+            help: null,
+            line: errorLine - 1,
+            col: errorCol
+          });
+        } else {
+          self.errors.push({
+            message: data.message,
+            help: data.help,
+            line: null,
+            col: null
+          });
+        }
+      } else {
+        $(document).trigger('error', data.message);
+        self.status('failed');
+      }
+    };
+
+    self.wasBatchExecuted = ko.observable(
+      typeof snippet.wasBatchExecuted != 'undefined' && snippet.wasBatchExecuted != null
+        ? snippet.wasBatchExecuted
+        : false
+    );
+    self.isReady = ko.computed(() => {
+      return (
+        (self.statementType() == 'text' &&
+          ((self.isSqlDialect() && self.statement() !== '') ||
+            (['jar', 'java', 'spark2', 'distcp'].indexOf(self.type()) == -1 &&
+              self.statement() !== '') ||
+            (['jar', 'java'].indexOf(self.type()) != -1 &&
+              (self.properties().app_jar() != '' && self.properties().class() != '')) ||
+            (['spark2'].indexOf(self.type()) != -1 && self.properties().jars().length > 0) ||
+            (['shell'].indexOf(self.type()) != -1 && self.properties().command_path().length > 0) ||
+            (['mapreduce'].indexOf(self.type()) != -1 && self.properties().app_jar().length > 0) ||
+            (['distcp'].indexOf(self.type()) != -1 &&
+              self.properties().source_path().length > 0 &&
+              self.properties().destination_path().length > 0))) ||
+        (self.statementType() == 'file' && self.statementPath().length > 0) ||
+        (self.statementType() == 'document' &&
+          self.associatedDocumentUuid() &&
+          self.associatedDocumentUuid().length > 0)
+      );
+    });
+    self.lastExecuted = ko.observable(
+      typeof snippet.lastExecuted != 'undefined' && snippet.lastExecuted != null
+        ? snippet.lastExecuted
+        : 0
+    );
+    self.lastAceSelectionRowOffset = ko.observable(snippet.lastAceSelectionRowOffset || 0);
+
+    self.executingBlockingOperation = null; // A ExecuteStatement()
+    self.showLongOperationWarning = ko.observable(false);
+    self.showLongOperationWarning.subscribe(newValue => {
+      if (newValue) {
+        hueAnalytics.convert('editor', 'showLongOperationWarning');
+      }
+    });
+
+    let longOperationTimeout = -1;
+
+    function startLongOperationTimeout() {
+      longOperationTimeout = window.setTimeout(() => {
+        self.showLongOperationWarning(true);
+      }, 2000);
+    }
+
+    function stopLongOperationTimeout() {
+      window.clearTimeout(longOperationTimeout);
+      self.showLongOperationWarning(false);
+    }
+
+    self.lastExecutedStatements = undefined;
+    self.lastExecutedSelectionRange = undefined;
+
+    self.execute = function(automaticallyTriggered) {
+      if (!automaticallyTriggered && self.ace()) {
+        const selectionRange = self.ace().getSelectionRange();
+
+        if (
+          self.lastExecutedSelectionRange &&
+          (selectionRange.start.row !== selectionRange.end.row &&
+            selectionRange.start.column !== selectionRange.end.column) &&
+          (selectionRange.start.row !== self.lastExecutedSelectionRange.start.row ||
+            selectionRange.start.column !== self.lastExecutedSelectionRange.start.column ||
+            selectionRange.end.row !== self.lastExecutedSelectionRange.end.row ||
+            selectionRange.end.column !== self.lastExecutedSelectionRange.end.column)
+        ) {
+          // Manual execute and there is a selection that is different from the last execute
+          self.result.cancelBatchExecution();
+        }
+        self.lastExecutedSelectionRange = selectionRange;
+      }
+
+      if (self.isCanceling()) {
+        return;
+      }
+      const now = new Date().getTime();
+      if (now - self.lastExecuted() < 1000 || !self.isReady()) {
+        return; // Prevent fast clicks
+      }
+
+      if (!automaticallyTriggered) {
+        // Do not cancel statements that are parts of a set of steps to execute (e.g. import). Only cancel statements as requested by user
+        if (self.status() === 'running' || self.status() === 'loading') {
+          self.cancel(); // TODO: Wait for cancel to finish
+        } else {
+          self.result.clear();
+        }
+      }
+
+      if (self.type() === 'impala') {
+        self.showExecutionAnalysis(false);
+        huePubSub.publish('editor.clear.execution.analysis');
+      }
+
+      self.status('running');
+      self.statusForButtons('executing');
+
+      if (self.isSqlDialect()) {
+        huePubSub.publish('editor.refresh.statement.locations', self);
+      }
+
+      self.lastExecutedStatements = self.statement();
+
+      if (self.ace()) {
+        huePubSub.publish('ace.set.autoexpand', { autoExpand: false, snippet: self });
+        const selectionRange = self.ace().getSelectionRange();
+        self.lastAceSelectionRowOffset(Math.min(selectionRange.start.row, selectionRange.end.row));
+      }
+
+      self.previousChartOptions = vm._getPreviousChartOptions(self);
+      $(document).trigger('executeStarted', { vm: vm, snippet: self });
+      self.lastExecuted(now);
+      $('.jHueNotify').remove();
+      hueAnalytics.log('notebook', 'execute/' + self.type());
+
+      notebook.forceHistoryInitialHeight(true);
+
+      if (self.result.handle()) {
+        self.close();
+      }
+
+      self.errors([]);
+      huePubSub.publish('editor.clear.highlighted.errors', self.ace());
+      self.result.clear();
+      self.progress(0);
+      self.jobs([]);
+      self.result.logs('');
+      self.result.statement_range({
+        start: {
+          row: 0,
+          column: 0
+        },
+        end: {
+          row: 0,
+          column: 0
+        }
+      });
+      notebook.historyCurrentPage(1);
+
+      // TODO: rename startLongOperationTimeout to startBlockingOperationTimeout
+      // TODO: stop blocking operation UI if there is one
+      // TODO: offer to stop blocking submit or fetch operation UI if there is one (add a new call to function for cancelBlockingOperation)
+      // TODO: stop current blocking operation if there is one
+      // TODO: handle jquery.dataTables.1.8.2.min.js:150 Uncaught TypeError: Cannot read property 'asSorting' of undefined on some cancels
+      // TODO: we should cancel blocking operation when leaving notebook (similar to unload())
+      // TODO: we should test when we go back to a query history of a blocking operation that we left
+      startLongOperationTimeout();
+
+      self.currentQueryTab('queryHistory');
+
+      self.executingBlockingOperation = $.post(
+        '/notebook/api/execute/' + self.type(),
+        {
+          notebook: vm.editorMode()
+            ? komapping.toJSON(notebook, NOTEBOOK_MAPPING)
+            : komapping.toJSON(notebook.getContext()),
+          snippet: komapping.toJSON(self.getContext())
+        },
+        data => {
+          try {
+            if (self.isSqlDialect() && data && data.handle) {
+              self.lastExecutedStatement(sqlStatementsParser.parse(data.handle.statement)[0]);
+            } else {
+              self.lastExecutedStatement(null);
+            }
+          } catch (e) {
+            self.lastExecutedStatement(null);
+          }
+          self.statusForButtons('executed');
+          huePubSub.publish('ace.set.autoexpand', { autoExpand: true, snippet: self });
+          stopLongOperationTimeout();
+
+          if (vm.editorMode() && data.history_id) {
+            if (!vm.isNotificationManager()) {
+              const url = vm.URLS.editor + '?editor=' + data.history_id;
+              vm.changeURL(url);
+            }
+            notebook.id(data.history_id);
+            notebook.uuid(data.history_uuid);
+            notebook.isHistory(true);
+            notebook.parentSavedQueryUuid(data.history_parent_uuid);
+          }
+
+          if (data.status === 0) {
+            self.result.handle(data.handle);
+            self.result.hasResultset(data.handle.has_result_set);
+            if (data.handle.sync) {
+              self.loadData(data.result, 100);
+              self.status('available');
+              self.progress(100);
+              self.result.endTime(new Date());
+            } else if (!notebook.unloaded()) {
+              self.checkStatus();
+            }
+            if (vm.isOptimizerEnabled()) {
+              huePubSub.publish('editor.upload.query', data.history_id);
+            }
+          } else {
+            self._ajaxError(data, self.execute);
+            notebook.isExecutingAll(false);
+          }
+
+          if (data.handle) {
+            if (vm.editorMode()) {
+              if (vm.isNotificationManager()) {
+                // Update task status
+                const tasks = $.grep(notebook.history(), row => {
+                  return row.uuid() == notebook.uuid();
+                });
+                if (tasks.length === 1) {
+                  tasks[0].status(self.status());
+                  self.result.logs(data.message);
+                }
+              } else {
+                notebook.history.unshift(
+                  notebook._makeHistoryRecord(
+                    undefined,
+                    data.handle.statement,
+                    self.lastExecuted(),
+                    self.status(),
+                    notebook.name(),
+                    notebook.uuid()
+                  )
+                );
+              }
+            }
+
+            if (data.handle.statements_count != null) {
+              self.result.statements_count(data.handle.statements_count);
+              self.result.statement_id(data.handle.statement_id);
+              self.result.previous_statement_hash(data.previous_statement_hash);
+
+              if (
+                data.handle.statements_count > 1 &&
+                data.handle.start != null &&
+                data.handle.end != null
+              ) {
+                self.result.statement_range({
+                  start: data.handle.start,
+                  end: data.handle.end
+                });
+              }
+            }
+          }
+        }
+      )
+        .fail((xhr, textStatus, errorThrown) => {
+          if (self.statusForButtons() != 'canceled' && xhr.status !== 502) {
+            // No error when manually canceled
+            $(document).trigger('error', xhr.responseText);
+          }
+          self.status('failed');
+          self.statusForButtons('executed');
+        })
+        .always(() => {
+          self.executingBlockingOperation = null;
+        });
+    };
+
+    self.reexecute = function() {
+      self.result.cancelBatchExecution();
+      self.execute();
+    };
+
+    self.formatEnabled = ko.pureComputed(() => {
+      return (
+        self.statement_raw && self.statement_raw() != null && self.statement_raw().length < 400000
+      ); // ie: 5000 lines at 80 chars per line
+    });
+
+    self.format = function() {
+      if (self.isSqlDialect()) {
+        apiHelper
+          .formatSql({
+            statements:
+              self.ace().getSelectedText() != ''
+                ? self.ace().getSelectedText()
+                : self.statement_raw()
+          })
+          .done(data => {
+            if (data.status == 0) {
+              if (self.ace().getSelectedText() != '') {
+                self
+                  .ace()
+                  .session.replace(
+                    self.ace().session.selection.getRange(),
+                    data.formatted_statements
+                  );
+              } else {
+                self.statement_raw(data.formatted_statements);
+                self.ace().setValue(self.statement_raw(), 1);
+              }
+            } else {
+              self._ajaxError(data);
+            }
+          });
+      }
+      hueAnalytics.log('notebook', 'format');
+    };
+
+    self.clear = function() {
+      hueAnalytics.log('notebook', 'clear');
+      self.ace().setValue('', 1);
+      self.result.clear();
+      self.status('ready');
+    };
+
+    self.explain = function() {
+      hueAnalytics.log('notebook', 'explain');
+
+      if (self.statement() == '' || self.status() == 'running' || self.status() === 'loading') {
+        return;
+      }
+
+      self.result.explanation('');
+      self.errors([]);
+      self.progress(0);
+      self.status('ready');
+
+      $.post(
+        '/notebook/api/explain',
+        {
+          notebook: komapping.toJSON(notebook.getContext()),
+          snippet: komapping.toJSON(self.getContext())
+        },
+        data => {
+          if (data.status == 0) {
+            self.currentQueryTab('queryExplain');
+            self.result.fetchedOnce(true);
+            self.result.explanation(data.explanation);
+          } else {
+            self._ajaxError(data);
+          }
+        }
+      );
+    };
+
+    let lastCompatibilityRequest;
+
+    self.checkCompatibility = function() {
+      self.hasSuggestion(null);
+      self.compatibilitySourcePlatform(COMPATIBILITY_SOURCE_PLATFORMS[self.type()]);
+      self.compatibilityTargetPlatform(
+        COMPATIBILITY_TARGET_PLATFORMS[self.type() === 'hive' ? 'impala' : 'hive']
+      );
+      self.queryCompatibility();
+    };
+
+    self.queryCompatibility = function(targetPlatform) {
+      apiHelper.cancelActiveRequest(lastCompatibilityRequest);
+
+      hueAnalytics.log('notebook', 'compatibility');
+      self.compatibilityCheckRunning(targetPlatform != self.type());
+      self.hasSuggestion(null);
+      const positionStatement = self.positionStatement();
+
+      lastCompatibilityRequest = $.post(
+        '/notebook/api/optimizer/statement/compatibility',
+        {
+          notebook: komapping.toJSON(notebook.getContext()),
+          snippet: komapping.toJSON(self.getContext()),
+          sourcePlatform: self.compatibilitySourcePlatform().value,
+          targetPlatform: self.compatibilityTargetPlatform().value
+        },
+        data => {
+          if (data.status === 0) {
+            self.aceErrorsHolder([]);
+            self.aceWarningsHolder([]);
+            self.suggestion(komapping.fromJS(data.query_compatibility));
+            if (self.suggestion().queryError && self.suggestion().queryError.errorString()) {
+              const match = ERROR_REGEX.exec(self.suggestion().queryError.errorString());
+              let line = null;
+              if (match) {
+                if (positionStatement) {
+                  line = positionStatement.location.first_line + parseInt(match[1]) + 1;
+                } else {
+                  line = parseInt(match[1]) - 1;
+                }
+              }
+              self.aceWarningsHolder.push({
+                message: self.suggestion().queryError.errorString(),
+                line: line,
+                col:
+                  match === null
+                    ? null
+                    : typeof match[3] !== 'undefined'
+                    ? parseInt(match[3])
+                    : null
+              });
+              self.status('with-optimizer-report');
+            }
+            if (self.suggestion().parseError()) {
+              const match = ERROR_REGEX.exec(self.suggestion().parseError());
+              self.aceErrorsHolder.push({
+                message: self.suggestion().parseError(),
+                line: match === null ? null : parseInt(match[1]) - 1,
+                col:
+                  match === null
+                    ? null
+                    : typeof match[3] !== 'undefined'
+                    ? parseInt(match[3])
+                    : null
+              });
+              self.status('with-optimizer-report');
+            }
+            self.showOptimizer(true);
+            self.hasSuggestion(true);
+          } else {
+            $(document).trigger('error', data.message);
+          }
+        }
+      )
+        .fail((xhr, textStatus, errorThrown) => {
+          if (xhr.status !== 502) {
+            $(document).trigger('error', xhr.responseText);
+          }
+        })
+        .always(() => {
+          self.compatibilityCheckRunning(false);
+        });
+    };
+
+    self.fetchResult = function(rows, startOver) {
+      if (typeof startOver == 'undefined') {
+        startOver = true;
+      }
+      self.fetchResultData(rows, startOver);
+      //self.fetchResultMetadata(rows);
+    };
+
+    self.isFetchingData = false;
+
+    self.fetchExecutionAnalysis = function() {
+      if (self.type() === 'impala') {
+        // TODO: Use real query ID
+        huePubSub.publish('editor.update.execution.analysis', {
+          analysisPossible: true,
+          compute: self.compute(),
+          queryId: notebook.getContext().id(),
+          name: self.jobs()[0] && self.jobs()[0].name
+        });
+      } else {
+        huePubSub.publish('editor.update.execution.analysis', {
+          analysisPossible: false
+        });
+      }
+    };
+
+    self.fetchResultData = function(rows, startOver) {
+      if (!self.isFetchingData) {
+        if (self.status() === 'available') {
+          startLongOperationTimeout();
+          self.isFetchingData = true;
+          hueAnalytics.log('notebook', 'fetchResult/' + rows + '/' + startOver);
+          $.post(
+            '/notebook/api/fetch_result_data',
+            {
+              notebook: komapping.toJSON(notebook.getContext()),
+              snippet: komapping.toJSON(self.getContext()),
+              rows: rows,
+              startOver: startOver
+            },
+            data => {
+              stopLongOperationTimeout();
+              data = JSON.bigdataParse(data);
+              if (data.status === 0) {
+                self.showExecutionAnalysis(true);
+                self.loadData(data.result, rows);
+              } else {
+                self._ajaxError(data, () => {
+                  self.isFetchingData = false;
+                  self.fetchResultData(rows, startOver);
+                });
+                $(document).trigger('renderDataError', { snippet: self });
+              }
+            },
+            'text'
+          )
+            .fail((xhr, textStatus, errorThrown) => {
+              if (xhr.status !== 502) {
+                $(document).trigger('error', xhr.responseText);
+              }
+            })
+            .always(() => {
+              self.isFetchingData = false;
+            });
+        } else {
+          huePubSub.publish('editor.snippet.result.normal', self);
+        }
+      }
+    };
+
+    self.loadData = function(result, rows) {
+      rows -= result.data.length;
+
+      if (result.data.length > 0) {
+        self.currentQueryTab('queryResults');
+      }
+
+      const _initialIndex = self.result.data().length;
+      const _tempData = [];
+      $.each(result.data, (index, row) => {
+        row.unshift(_initialIndex + index + 1);
+        self.result.data.push(row);
+        _tempData.push(row);
+      });
+
+      if (self.result.rows() == null || (self.result.rows() + '').indexOf('+') != -1) {
+        self.result.rows(self.result.data().length + (result.has_more ? '+' : ''));
+      }
+
+      self.result.images(
+        typeof result.images != 'undefined' && result.images != null ? result.images : []
+      );
+
+      huePubSub.publish('editor.render.data', {
+        data: _tempData,
+        snippet: self,
+        initial: _initialIndex == 0
+      });
+
+      if (!self.result.fetchedOnce()) {
+        result.meta.unshift({ type: 'INT_TYPE', name: '', comment: null });
+        self.result.meta(result.meta);
+        self.result.type(result.type);
+        self.result.fetchedOnce(true);
+      }
+
+      self.result.meta().forEach(meta => {
+        if (
+          $.inArray(meta.type, [
+            'TINYINT_TYPE',
+            'SMALLINT_TYPE',
+            'INT_TYPE',
+            'BIGINT_TYPE',
+            'FLOAT_TYPE',
+            'DOUBLE_TYPE',
+            'DECIMAL_TYPE'
+          ]) > -1
+        ) {
+          meta.cssClass = 'sort-numeric';
+        } else if ($.inArray(meta.type, ['TIMESTAMP_TYPE', 'DATE_TYPE', 'DATETIME_TYPE']) > -1) {
+          meta.cssClass = 'sort-date';
+        } else {
+          meta.cssClass = 'sort-string';
+        }
+      });
+
+      self.result.hasMore(result.has_more);
+
+      if (result.has_more && rows > 0) {
+        setTimeout(() => {
+          self.fetchResultData(rows, false);
+        }, 500);
+      } else if (
+        !vm.editorMode() &&
+        !notebook.isPresentationMode() &&
+        notebook.snippets()[notebook.snippets().length - 1] == self
+      ) {
+        notebook.newSnippet();
+      }
+    };
+
+    self.fetchResultMetadata = function() {
+      $.post(
+        '/notebook/api/fetch_result_metadata',
+        {
+          notebook: komapping.toJSON(notebook.getContext()),
+          snippet: komapping.toJSON(self.getContext())
+        },
+        data => {
+          if (data.status == 0) {
+            self.result.meta(data.result.meta);
+          } else {
+            $(document).trigger('error', data.message);
+          }
+        }
+      ).fail((xhr, textStatus, errorThrown) => {
+        if (xhr.status !== 502) {
+          $(document).trigger('error', xhr.responseText);
+        }
+        self.status('failed');
+      });
+    };
+
+    self.fetchResultSize = function(n, query_id) {
+      $.post(
+        '/notebook/api/fetch_result_size',
+        {
+          notebook: komapping.toJSON(notebook.getContext()),
+          snippet: komapping.toJSON(self.getContext())
+        },
+        data => {
+          if (query_id == notebook.id()) {
+            // If still on the same result
+            if (data.status == 0) {
+              if (data.result.rows != null) {
+                self.result.rows(data.result.rows);
+              } else if (self.type() == 'impala' && n > 0) {
+                setTimeout(() => {
+                  self.fetchResultSize(n - 1, query_id);
+                }, 1000);
+              }
+            } else if (data.status == 5) {
+              // No supported yet for this snippet
+            } else {
+              //$(document).trigger("error", data.message);
+            }
+          }
+        }
+      ).fail((xhr, textStatus, errorThrown) => {
+        //$(document).trigger("error", xhr.responseText);
+      });
+    };
+
+    self.checkStatus = function() {
+      $.post(
+        '/notebook/api/check_status',
+        {
+          notebook: komapping.toJSON(notebook.getContext()),
+          snippet: komapping.toJSON(self.getContext())
+        },
+        data => {
+          if (self.statusForButtons() == 'canceling' || self.status() == 'canceled') {
+            // Query was canceled in the meantime, do nothing
+          } else {
+            self.result.endTime(new Date());
+
+            if (data.status === 0) {
+              self.status(data.query_status.status);
+
+              if (
+                self.status() == 'running' ||
+                self.status() == 'starting' ||
+                self.status() == 'waiting'
+              ) {
+                const delay = self.result.executionTime() > 45000 ? 5000 : 1000; // 5s if more than 45s
+                if (!notebook.unloaded()) {
+                  self.checkStatusTimeout = setTimeout(self.checkStatus, delay);
+                }
+              } else if (self.status() === 'available') {
+                self.fetchResult(100);
+                self.progress(100);
+                if (self.isSqlDialect()) {
+                  if (self.result.handle().has_result_set) {
+                    const _query_id = notebook.id();
+                    setTimeout(() => {
+                      // Delay until we get IMPALA-5555
+                      self.fetchResultSize(10, _query_id);
+                    }, 2000);
+                    self.checkDdlNotification(); // DDL CTAS with Impala
+                  } else if (self.lastExecutedStatement()) {
+                    self.checkDdlNotification();
+                  } else {
+                    self.onDdlExecute();
+                  }
+                }
+                if (notebook.isExecutingAll()) {
+                  notebook.executingAllIndex(notebook.executingAllIndex() + 1);
+                  if (notebook.executingAllIndex() < notebook.snippets().length) {
+                    notebook.snippets()[notebook.executingAllIndex()].execute();
+                  } else {
+                    notebook.isExecutingAll(false);
+                  }
+                }
+                if (!self.result.handle().has_more_statements && vm.successUrl()) {
+                  window.location.href = vm.successUrl(); // Not used anymore in Hue 4
+                }
+              } else if (self.status() === 'success') {
+                self.progress(99);
+              }
+            } else if (data.status === -3) {
+              self.status('expired');
+              notebook.isExecutingAll(false);
+            } else {
+              self._ajaxError(data);
+              notebook.isExecutingAll(false);
+            }
+            self.getLogs(); // Need to execute at the end, because updating the status impacts log progress results
+          }
+        }
+      ).fail((xhr, textStatus, errorThrown) => {
+        if (xhr.status !== 502) {
+          $(document).trigger('error', xhr.responseText || textStatus);
+        }
+        self.status('failed');
+        notebook.isExecutingAll(false);
+      });
+    };
+
+    self.checkDdlNotification = function() {
+      if (
+        self.lastExecutedStatement() &&
+        /ALTER|CREATE|DELETE|DROP|GRANT|INSERT|LOAD|SET|TRUNCATE|UPDATE|UPSERT|USE/i.test(
+          self.lastExecutedStatement().firstToken
+        )
+      ) {
+        self.onDdlExecute();
+      } else {
+        window.clearTimeout(self.executeNextTimeout);
+      }
+    };
+
+    self.isCanceling = ko.observable(false);
+
+    self.cancel = function() {
+      window.clearTimeout(self.executeNextTimeout);
+      self.isCanceling(true);
+      if (self.checkStatusTimeout != null) {
+        clearTimeout(self.checkStatusTimeout);
+        self.checkStatusTimeout = null;
+      }
+      hueAnalytics.log('notebook', 'cancel');
+
+      if (self.executingBlockingOperation != null) {
+        self.executingBlockingOperation.abort();
+        self.executingBlockingOperation = null;
+      }
+
+      if ($.isEmptyObject(self.result.handle())) {
+        // Query was not even submitted yet
+        self.statusForButtons('canceled');
+        self.status('failed');
+        self.isCanceling(false);
+        notebook.isExecutingAll(false);
+      } else {
+        self.statusForButtons('canceling');
+        $.post(
+          '/notebook/api/cancel_statement',
+          {
+            notebook: komapping.toJSON(notebook.getContext()),
+            snippet: komapping.toJSON(self.getContext())
+          },
+          data => {
+            self.statusForButtons('canceled');
+            if (data.status == 0) {
+              self.status('canceled');
+              notebook.isExecutingAll(false);
+            } else {
+              self._ajaxError(data);
+            }
+          }
+        )
+          .fail((xhr, textStatus, errorThrown) => {
+            if (xhr.status !== 502) {
+              $(document).trigger('error', xhr.responseText);
+            }
+            self.statusForButtons('canceled');
+            self.status('failed');
+            notebook.isExecutingAll(false);
+          })
+          .always(() => {
+            self.isCanceling(false);
+          });
+      }
+    };
+
+    self.close = function() {
+      if (self.checkStatusTimeout != null) {
+        clearTimeout(self.checkStatusTimeout);
+        self.checkStatusTimeout = null;
+      }
+
+      $.post(
+        '/notebook/api/close_statement',
+        {
+          notebook: komapping.toJSON(notebook.getContext()),
+          snippet: komapping.toJSON(self.getContext())
+        },
+        data => {
+          if (data.status == 0) {
+            // self.status('closed'); // Keep as 'running' as currently it happens before running a new query
+          } else {
+            // self._ajaxError(data);
+          }
+        }
+      ).fail((xhr, textStatus, errorThrown) => {
+        if (xhr.status !== 502) {
+          // $(document).trigger("error", xhr.responseText);
+        }
+        // self.status('failed'); // Can conflict with slow close and new query execution
+      });
+    };
+
+    self.getLogs = function() {
+      $.post(
+        '/notebook/api/get_logs',
+        {
+          notebook: komapping.toJSON(notebook.getContext()),
+          snippet: komapping.toJSON(self.getContext()),
+          from: self.result.logLines,
+          jobs: komapping.toJSON(self.jobs, { ignore: ['percentJob'] }),
+          full_log: self.result.logs
+        },
+        data => {
+          if (data.status == 1) {
+            // Append errors to the logs
+            data.status = 0;
+            data.logs = data.message;
+          }
+          if (data.status == 0) {
+            if (data.logs.length > 0) {
+              const logs = data.logs.split('\n');
+              self.result.logLines += logs.length;
+              const oldLogs = self.result.logs();
+              if (
+                data.logs &&
+                (oldLogs === '' ||
+                  (self.wasBatchExecuted() && data.logs.indexOf('Unable to locate') == -1) ||
+                  data.isFullLogs)
+              ) {
+                self.result.logs(data.logs);
+              } else {
+                self.result.logs(oldLogs + '\n' + data.logs);
+              }
+            }
+
+            self.jobs().forEach(job => {
+              if (typeof job.percentJob === 'undefined') {
+                job.percentJob = ko.observable(-1);
+              }
+            });
+
+            if (data.jobs && data.jobs.length > 0) {
+              data.jobs.forEach(job => {
+                const _found = ko.utils.arrayFilter(self.jobs(), item => {
+                  return item.name === job.name;
+                });
+                if (_found.length === 0) {
+                  if (typeof job.percentJob === 'undefined') {
+                    job.percentJob = ko.observable(-1);
+                  } else {
+                    job.percentJob = ko.observable(job.percentJob);
+                  }
+                  self.jobs.push(job);
+                } else if (typeof job.percentJob !== 'undefined') {
+                  for (let i = 0; i < _found.length; i++) {
+                    _found[i].percentJob(job.percentJob);
+                  }
+                }
+              });
+              self.jobs().forEach(job => {
+                const _found = ko.utils.arrayFilter(self.jobs(), item => {
+                  return item.name === job.name;
+                });
+                if (_found.length === 0) {
+                  self.jobs.remove(job);
+                }
+              });
+            }
+            if (self.status() == 'running') {
+              // Maybe the query finished or failed in the meantime
+              self.progress(data.progress);
+            }
+          } else {
+            self._ajaxError(data);
+          }
+        }
+      ).fail((xhr, textStatus, errorThrown) => {
+        if (xhr.status !== 502) {
+          $(document).trigger('error', xhr.responseText || textStatus);
+        }
+        self.status('failed');
+      });
+    };
+
+    self.uploadQueryHistory = function(n) {
+      hueAnalytics.log('notebook', 'upload_query_history');
+
+      $.post(
+        '/metadata/api/optimizer/upload/history',
+        {
+          n: typeof n != 'undefined' ? n : null,
+          sourcePlatform: self.type()
+        },
+        data => {
+          if (data.status == 0) {
+            $(document).trigger(
+              'info',
+              data.upload_history[self.type()].count +
+                ' queries uploaded successfully. Processing them...'
+            );
+            self.watchUploadStatus(data.upload_history[self.type()].status.workloadId);
+          } else {
+            $(document).trigger('error', data.message);
+          }
+        }
+      );
+    };
+
+    self.uploadQuery = function(query_id) {
+      $.post('/metadata/api/optimizer/upload/query', {
+        query_id: query_id,
+        sourcePlatform: self.type()
+      });
+    };
+
+    self.uploadTableStats = function(options) {
+      hueAnalytics.log('notebook', 'load_table_stats');
+      if (options.showProgress) {
+        $(document).trigger('info', 'Preparing table data...');
+      }
+
+      $.post(
+        '/metadata/api/optimizer/upload/table_stats',
+        {
+          db_tables: komapping.toJSON(
+            $.map(options.activeTables, table => {
+              return table.databaseName + '.' + table.tableName;
+            })
+          ),
+          sourcePlatform: komapping.toJSON(self.type()),
+          with_ddl: komapping.toJSON(true),
+          with_table_stats: komapping.toJSON(true),
+          with_columns_stats: komapping.toJSON(true)
+        },
+        data => {
+          if (data.status == 0) {
+            if (options.showProgress) {
+              $(document).trigger(
+                'info',
+                $.map(options.activeTables, table => {
+                  return table.tableName;
+                }) + ' stats sent to analyse'
+              );
+            }
+            if (data.upload_table_ddl && options.showProgress) {
+              // With showProgress only currently as can be very slow
+              self.watchUploadStatus(data.upload_table_ddl.status.workloadId, options.showProgress);
+            }
+          } else if (options.showProgress) {
+            $(document).trigger('error', data.message);
+          }
+        }
+      ).always(() => {
+        if (options.callback) {
+          options.callback();
+        }
+      });
+    };
+
+    self.watchUploadStatus = function(workloadId, showProgress) {
+      $.post(
+        '/metadata/api/optimizer/upload/status',
+        {
+          workloadId: workloadId
+        },
+        data => {
+          if (data.status == 0) {
+            if (showProgress) {
+              $(document).trigger('info', 'Query processing: ' + data.upload_status.status.state);
+            }
+            if (['WAITING', 'IN_PROGRESS'].indexOf(data.upload_status.status.state) != -1) {
+              window.setTimeout(() => {
+                self.watchUploadStatus(workloadId);
+              }, 2000);
+            } else if (showProgress) {
+              $(document).trigger(
+                'warn',
+                data.upload_status.status.statusMsg +
+                  (data.upload_status.status.failedQueries > 0
+                    ? '. ' +
+                      data.upload_status.status.failQueryDetails.map(query => {
+                        return query.error;
+                      })
+                    : '')
+              );
+            }
+          } else if (showProgress) {
+            $(document).trigger('error', data.message);
+          }
+        }
+      );
+    };
+
+    self.getSimilarQueries = function() {
+      hueAnalytics.log('notebook', 'get_query_similarity');
+
+      $.post(
+        '/notebook/api/optimizer/statement/similarity',
+        {
+          notebook: komapping.toJSON(notebook.getContext()),
+          snippet: komapping.toJSON(self.getContext()),
+          sourcePlatform: self.type()
+        },
+        data => {
+          if (data.status == 0) {
+            console.log(data.statement_similarity);
+          } else {
+            $(document).trigger('error', data.message);
+          }
+        }
+      );
+    };
+
+    self.autocompleter = new AceAutocompleteWrapper({
+      snippet: self,
+      user: vm.user,
+      optEnabled: false,
+      timeout: vm.autocompleteTimeout
+    });
+
+    self.init = function() {
+      if ((self.status() == 'running' || self.status() == 'available') && notebook.isHistory()) {
+        self.checkStatus();
+      } else if (self.status() == 'loading') {
+        self.status('failed');
+        self.progress(0);
+        self.jobs([]);
+      } else if (self.status() == 'ready-execute') {
+        self.execute();
+      }
+    };
+
+    self.onKeydownInVariable = function(context, e) {
+      if ((e.ctrlKey || e.metaKey) && e.which === 13) {
+        // Ctrl-enter
+        self.ace().commands.commands['execute'].exec();
+      } else if ((e.ctrlKey || e.metaKey) && e.which === 83) {
+        // Ctrl-s
+        self.ace().commands.commands['save'].exec();
+        e.preventDefault(); // Prevent browser page save dialog
+      }
+      return true;
+    };
+  }
+
+  renderMarkdown() {
+    return this.statement_raw().replace(/([^$]*)([$]+[^$]*[$]+)?/g, (a, textRepl, code) => {
+      return markdown.toHTML(textRepl).replace(/^<p>|<\/p>$/g, '') + (code ? code : '');
+    });
+  }
+
+  toggleAllResultColumns(linkElement) {
+    const $t = $(linkElement)
+      .parents('.snippet')
+      .find('table.resultTable:eq(0)');
+    const dt = $t.hueDataTable();
+    dt.fnToggleAllCols(linkElement.checked);
+    dt.fnDraw();
+  }
+
+  toggleResultColumn(linkElement, index) {
+    const $t = $(linkElement)
+      .parents('.snippet')
+      .find('table.resultTable:eq(0)');
+    const dt = $t.hueDataTable();
+    dt.fnSetColumnVis(index, linkElement.checked);
+  }
+  scrollToResultColumn(linkElement) {
+    const $resultTable = $(linkElement)
+      .parents('.snippet')
+      .find('table.resultTable:eq(0)');
+    const _text = $.trim($(linkElement).text());
+    const _col = $resultTable.find('th').filter(function() {
+      return $.trim($(this).text()) === _text;
+    });
+    $resultTable.find('.columnSelected').removeClass('columnSelected');
+    const _colSel = $resultTable.find('tr th:nth-child(' + (_col.index() + 1) + ')');
+    if (_colSel.length > 0) {
+      $resultTable.find('tr td:nth-child(' + (_col.index() + 1) + ')').addClass('columnSelected');
+      $resultTable
+        .parent()
+        .scrollLeft(
+          _colSel.position().left +
+            $resultTable.parent().scrollLeft() -
+            $resultTable.parent().offset().left -
+            30
+        );
+      $resultTable.data('scrollToCol', _col.index());
+      $resultTable.data('scrollToRow', null);
+      $resultTable.data('scrollAnimate', true);
+      $resultTable.parent().trigger('scroll');
+    }
+  }
+}
+
+export default Snippet;

File diff suppressed because it is too large
+ 0 - 0
desktop/core/src/desktop/js/ext/markdown.min.js


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

@@ -54,7 +54,7 @@ import SidePanelViewModel from 'sidePanelViewModel';
 import TopNavViewModel from 'topNavViewModel';
 
 // TODO: Remove from global scope
-import EditorViewModel from 'apps/notebook/notebook.ko'; // In history, indexer, importer, editor etc.
+import EditorViewModel from 'apps/notebook/editorViewModel'; // In history, indexer, importer, editor etc.
 import globalSearchParser from 'parse/globalSearchParser'; // ko inline autocomp
 import HdfsAutocompleter from 'utils/hdfsAutocompleter';
 import solrFormulaParser from 'parse/solrFormulaParser'; // simple ace editor

+ 3 - 0
desktop/core/src/desktop/js/utils/spec/hdfsAutocompleterSpec.js

@@ -14,6 +14,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import 'spec/jquery.test';
+import 'spec/jquery.plugins';
+
 import ko from 'knockout';
 
 import apiHelper from 'api/apiHelper';

File diff suppressed because it is too large
+ 0 - 0
desktop/core/src/desktop/static/desktop/ext/js/markdown.min.js


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

@@ -30,6 +30,8 @@
 (function () {
   window.AUTOCOMPLETE_TIMEOUT = ${ conf.EDITOR_AUTOCOMPLETE_TIMEOUT.get() };
 
+  window.BANNER_TOP_HTML = '${ conf.CUSTOM.BANNER_TOP_HTML.get() }';
+
   window.CACHEABLE_TTL = {
     default: ${ conf.CUSTOM.CACHEABLE_TTL.get() },
     optimizer: ${ OPTIMIZER.CACHEABLE_TTL.get() }
@@ -163,6 +165,12 @@
       documents: '${ _('Documents') }',
       editor: '${ _('Editor') }',
     },
+    notebook: {
+      dropEditorHoverText: '${_('Drop a SQL file here')}',
+      dropNotebookHoverText: '${_('Drop iPython/Zeppelin notebooks here')}',
+      noResults: '${_('No results found.')}',
+      of: '${_('of')}'
+    },
     selectize: {
       choose: "${ _('Choose...') }",
       editTags: "${ _('Edit tags') }"

+ 151 - 1776
desktop/libs/notebook/src/notebook/templates/editor_components.mako

@@ -1144,7 +1144,6 @@ ${ sqlSyntaxDropdown.sqlSyntaxDropdown() }
             </a>
           </div>
           <a class="btn" data-bind="click: function() { getExternalStatement(); }"><i class="fa fa-lg fa-refresh"></i></a>
-          ##<a class="btn" data-bind="tooltip: { placement: 'bottom', title: 'Save content back to file' }, click: function() { huePubSub.publish('show.saveToFile.modal'); }"><i class="fa fa-save"></i></a>
           <!-- /ko -->
         </div>
         <div class="clearfix margin-bottom-20"></div>
@@ -1442,7 +1441,7 @@ ${ sqlSyntaxDropdown.sqlSyntaxDropdown() }
       <thead>
         <tr>
           <th width="16">
-            <input class="all-meta-checked no-margin-top" type="checkbox" data-bind="enable: !result.isMetaFilterVisible() && result.filteredMeta().length > 0, event: { change: function(){ toggleAllColumns($element, $data); result.clickFilteredMetaCheck() } }, checked: result.filteredMetaChecked" />
+            <input class="all-meta-checked no-margin-top" type="checkbox" data-bind="enable: !result.isMetaFilterVisible() && result.filteredMeta().length > 0, event: { change: function(){ toggleAllResultColumns($element); result.clickFilteredMetaCheck() } }, checked: result.filteredMetaChecked" />
           </th>
           <th colspan="2" class="nav-header-like">
             <span class="meta-title pointer" data-bind="click: function(){ result.isMetaFilterVisible(true); }, attr: {title: result.filteredMeta().length }">${_('columns')}</span>
@@ -1468,8 +1467,8 @@ ${ sqlSyntaxDropdown.sqlSyntaxDropdown() }
       </thead>
       <tbody class="unstyled filtered-meta" data-bind="foreach: result.filteredMeta">
         <tr data-bind="visible: name != ''">
-          <td><input class="no-margin-top" type="checkbox" data-bind="event: { change: function(){ toggleColumn($element, originalIndex, $parent);} }, checked: checked" /></td>
-          <td><a class="pointer" data-bind="click: function(){ scrollToColumn($element, $data.originalIndex); }, attr: { title: name + ' - ' + type}"><span data-bind="text: name"></span></a></td>
+          <td><input class="no-margin-top" type="checkbox" data-bind="event: { change: function() { $parent.toggleResultColumn($element, originalIndex);} }, checked: checked" /></td>
+          <td><a class="pointer" data-bind="click: function(){ $parent.scrollToResultColumn($element); }, attr: { title: name + ' - ' + type}"><span data-bind="text: name"></span></a></td>
           <td><span data-bind="text: type" class="muted margin-left-20"></span></td>
         </tr>
       </tbody>
@@ -1541,37 +1540,37 @@ ${ sqlSyntaxDropdown.sqlSyntaxDropdown() }
               <div data-bind="visible: hasDataForChart">
                 <!-- ko if: chartType() == ko.HUE_CHARTS.TYPES.PIECHART -->
                 <div data-bind="attr:{'id': 'pieChart_'+id()}, pieChart: {data: {counts: result.data, sorting: chartSorting(), snippet: $data, limit: chartLimit()}, fqs: ko.observableArray([]),
-                      transformer: editorPieChartDataTransformer, maxWidth: 350, parentSelector: '.chart-container' }, visible: chartType() == ko.HUE_CHARTS.TYPES.PIECHART" class="chart"></div>
+                      transformer: $root.ChartTransformers.pie, maxWidth: 350, parentSelector: '.chart-container' }, visible: chartType() == ko.HUE_CHARTS.TYPES.PIECHART" class="chart"></div>
                 <!-- /ko -->
 
                 <!-- ko if: chartType() == ko.HUE_CHARTS.TYPES.BARCHART -->
                 <div data-bind="attr:{'id': 'barChart_'+id()}, barChart: {skipWindowResize: true, datum: {counts: result.data, sorting: chartSorting(), snippet: $data, limit: chartLimit()}, fqs: ko.observableArray([]), hideSelection: true, enableSelection: false, hideStacked: hideStacked,
-                      transformer: editorMultiSerieDataTransformer, stacked: false, showLegend: true, isPivot: typeof chartXPivot() !== 'undefined', type: chartTimelineType},  stacked: true, showLegend: true, visible: chartType() == ko.HUE_CHARTS.TYPES.BARCHART" class="chart"></div>
+                      transformer: $root.ChartTransformers.multiSerie, stacked: false, showLegend: true, isPivot: typeof chartXPivot() !== 'undefined', type: chartTimelineType},  stacked: true, showLegend: true, visible: chartType() == ko.HUE_CHARTS.TYPES.BARCHART" class="chart"></div>
                 <!-- /ko -->
 
                 <!-- ko if: chartType() == ko.HUE_CHARTS.TYPES.LINECHART -->
                 <div data-bind="attr:{'id': 'lineChart_'+id()}, lineChart: {datum: {counts: result.data, sorting: chartSorting(), snippet: $data, limit: chartLimit()},
-                      transformer: editorMultiSerieDataTransformer, showControls: false, enableSelection: false }, visible: chartType() == ko.HUE_CHARTS.TYPES.LINECHART" class="chart"></div>
+                      transformer: $root.ChartTransformers.multiSerie, showControls: false, enableSelection: false }, visible: chartType() == ko.HUE_CHARTS.TYPES.LINECHART" class="chart"></div>
                 <!-- /ko -->
 
                 <!-- ko if: chartType() == ko.HUE_CHARTS.TYPES.TIMELINECHART -->
                 <div data-bind="attr:{'id': 'timelineChart_'+id()}, timelineChart: {type: chartTimelineType, skipWindowResize: true, datum: {counts: result.data, sorting: chartSorting(), snippet: $data, limit: chartLimit()}, fqs: ko.observableArray([]), hideSelection: true, enableSelection: false, hideStacked: hideStacked,
-                      transformer: editorTimelineChartDataTransformer, stacked: false, showLegend: true}, visible: chartType() == ko.HUE_CHARTS.TYPES.TIMELINECHART" class="chart"></div>
+                      transformer: $root.ChartTransformers.timeline, stacked: false, showLegend: true}, visible: chartType() == ko.HUE_CHARTS.TYPES.TIMELINECHART" class="chart"></div>
                 <!-- /ko -->
 
                 <!-- ko if: chartType() == ko.HUE_CHARTS.TYPES.MAP -->
                 <div data-bind="attr:{'id': 'leafletMapChart_'+id()}, leafletMapChart: {datum: {counts: result.data, sorting: chartSorting(), snippet: $data, limit: chartLimit()},
-                      transformer: editorLeafletMapChartDataTransformer, showControls: false, height: 380, visible: chartType() == ko.HUE_CHARTS.TYPES.MAP, forceRedraw: true}" class="chart"></div>
+                      transformer: $root.ChartTransformers.leafletMap, showControls: false, height: 380, visible: chartType() == ko.HUE_CHARTS.TYPES.MAP, forceRedraw: true}" class="chart"></div>
                 <!-- /ko -->
 
                 <!-- ko if: chartType() == ko.HUE_CHARTS.TYPES.GRADIENTMAP -->
                 <div data-bind="attr:{'id': 'gradientMapChart_'+id()}, mapChart: {data: {counts: result.data, sorting: chartSorting(), snippet: $data, scope: chartScope(), limit: chartLimit()},
-                      transformer: editorMapChartDataTransformer, isScale: true, showControls: false, height: 380, maxWidth: 750, parentSelector: '.chart-container', visible: chartType() == ko.HUE_CHARTS.TYPES.GRADIENTMAP}" class="chart"></div>
+                      transformer: $root.ChartTransformers.map, isScale: true, showControls: false, height: 380, maxWidth: 750, parentSelector: '.chart-container', visible: chartType() == ko.HUE_CHARTS.TYPES.GRADIENTMAP}" class="chart"></div>
                 <!-- /ko -->
 
                 <!-- ko if: chartType() == ko.HUE_CHARTS.TYPES.SCATTERCHART -->
                 <div data-bind="attr:{'id': 'scatterChart_'+id()}, scatterChart: {datum: {counts: result.data, snippet: $data, limit: chartLimit()},
-                      transformer: editorScatterChartDataTransformer, maxWidth: 350, y: chartYSingle(), x: chartX(), size: chartScatterSize(), group: chartScatterGroup() }, visible: chartType() == ko.HUE_CHARTS.TYPES.SCATTERCHART" class="chart"></div>
+                      transformer: $root.ChartTransformers.scatter, maxWidth: 350, y: chartYSingle(), x: chartX(), size: chartScatterSize(), group: chartScatterGroup() }, visible: chartType() == ko.HUE_CHARTS.TYPES.SCATTERCHART" class="chart"></div>
                 <!-- /ko -->
               </div>
             </div>
@@ -1598,12 +1597,12 @@ ${ sqlSyntaxDropdown.sqlSyntaxDropdown() }
       }"></div>
     </div>
     <div class="span6">
-      <div data-bind="html: renderMarkdown(statement_raw(), id()), attr: {'id': 'liveMD' + id()}"></div>
+      <div data-bind="html: renderMarkdown, attr: {'id': 'liveMD' + id()}"></div>
     </div>
   </div>
   <!-- /ko -->
   <!-- ko if: $root.isPresentationMode() -->
-  <div data-bind="html: renderMarkdown(statement_raw(), id())"></div>
+  <div data-bind="html: renderMarkdown"></div>
   <!-- /ko -->
 </script>
 
@@ -2097,1782 +2096,158 @@ ${ sqlSyntaxDropdown.sqlSyntaxDropdown() }
 <%def name="commonJS(is_embeddable=False, bindableElement='editorComponents', suffix='')">
 
 <script type="text/javascript">
-  var HUE_PUB_SUB_EDITOR_ID = (window.location.pathname.indexOf('notebook') > -1) ? 'notebook' : 'editor';
-  % if is_embeddable:
-  var MAIN_SCROLLABLE = '.page-content';
-  % else:
-  var MAIN_SCROLLABLE = '.content-panel';
-  var shareViewModel = initSharing("#documentShareModal");
-  % endif
-
-  var isLeftNavOpen = false;
-  huePubSub.subscribe('left.nav.open.toggle', function (val) {
-    isLeftNavOpen = val;
-  }, HUE_PUB_SUB_EDITOR_ID);
+  window.EDITOR_BINDABLE_ELEMENT = '#${ bindableElement }';
 
-  huePubSub.subscribe('split.panel.resized', function (val) {
-    huePubSub.publish('recalculate.name.description.width');
-  }, HUE_PUB_SUB_EDITOR_ID);
-
-  var showHoverMsg = function (e) {
-    var dt = null;
-    if (e) {
-      dt = e.dataTransfer;
-    }
-    if (!isLeftNavOpen && (!dt || (dt.types && (dt.types.indexOf ? dt.types.indexOf('Files') != -1 : dt.types.contains('Files'))))) {
-      $('#${ bindableElement }').find(".hoverMsg").removeClass("hide");
-    }
-  };
+  window.EDITOR_SUFFIX = '${ suffix }';
 
-  var hideHoverMsg = function (vm) {
-    if (vm.editorMode()){
-      $('#${ bindableElement }').find(".hoverText").html("${_('Drop a SQL file here')}");
-    } else {
-      $('#${ bindableElement }').find(".hoverText").html("${_('Drop iPython/Zeppelin notebooks here')}");
-    }
-    $('#${ bindableElement }').find(".hoverMsg").addClass("hide");
-  };
-
-  function renderMarkdown(text, snippetId) {
-    text = text.replace(/([^$]*)([$]+[^$]*[$]+)?/g, function (a, text, code) {
-      return markdown.toHTML(text).replace(/^<p>|<\/p>$/g, '') + (code ? code : '');
-    });
-    return text;
-  }
+  var HUE_PUB_SUB_EDITOR_ID = (window.location.pathname.indexOf('notebook') > -1) ? 'notebook' : 'editor';
 
-  function prepareShareModal () {
-    var selectedNotebookUuid = this.selectedNotebook() && this.selectedNotebook().uuid();
-    shareViewModel.setDocUuid(this.selectedNotebook().uuid());
-    openShareModal();
-  };
-
-  function createHueDatatable(el, snippet, vm) {
-    var DATATABLES_MAX_HEIGHT = 330;
-    var invisibleRows = 10;
-    if (snippet.result && snippet.result.data() && snippet.result.data().length) {
-      var cols = snippet.result.data()[0].length;
-      invisibleRows = cols > 200 ? 10 : (cols > 30 ? 50 : 100);
-    }
-    var _dt = $(el).hueDataTable({
-      i18n: {
-        NO_RESULTS: "${_('No results found.')}",
-        OF: "${_('of')}"
+  window.EDITOR_VIEW_MODEL_OPTIONS = $.extend(${ options_json | n,unicode,antixss }, {
+    huePubSubId: HUE_PUB_SUB_EDITOR_ID,
+    user: '${ user.username }',
+    userId: ${ user.id },
+    suffix: '${ suffix }',
+    assistAvailable: true,
+    autocompleteTimeout: AUTOCOMPLETE_TIMEOUT,
+    snippetViewSettings: {
+      default: {
+        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
+        aceMode: 'ace/mode/sql',
+        snippetIcon: 'fa-database',
+        sqlDialect: true
       },
-      fnDrawCallback: function (oSettings) {
-        if (vm.editorMode()) {
-          $('#queryResults').removeAttr('style');
-          DATATABLES_MAX_HEIGHT = $(window).height() - $(el).parent().offset().top - 40;
-          $(el).parents('.dataTables_wrapper').css('overflow-x', 'hidden');
-          $(el).jHueHorizontalScrollbar();
-          $(el).parents('.dataTables_wrapper').jHueScrollLeft();
-        }
-        else {
-          if ($(el).data('fnDraws') === 1) {
-            $(el).parents(".dataTables_wrapper").jHueTableScroller({
-              maxHeight: DATATABLES_MAX_HEIGHT,
-              heightAfterCorrection: 0
-            });
-          }
-        }
+      code: {
+        placeHolder: '${ _("Example: 1 + 1, or press CTRL + space") }',
+        snippetIcon: 'fa-code'
       },
-      scrollable: vm.editorMode() && !vm.isPresentationMode() ? MAIN_SCROLLABLE : '.dataTables_wrapper',
-      contained: !vm.editorMode() || vm.isPresentationMode(),
-      forceInvisible: invisibleRows
-    });
-
-    window.setTimeout(function () {
-      if (vm.editorMode()) {
-        $(el).parents('.dataTables_wrapper').css('overflow-x', 'hidden');
-        % if conf.CUSTOM.BANNER_TOP_HTML.get():
-          var bannerTopHeight = 30;
-        % else:
-          var bannerTopHeight = 0;
-        % endif
-        $(el).jHueTableExtender2({
-          mainScrollable: MAIN_SCROLLABLE,
-          fixedFirstColumn: vm.editorMode(),
-          % if is_embeddable:
-          stickToTopPosition: 48 + bannerTopHeight + ${ conf.CUSTOM.BANNER_TOP_HTML.get() and '0' or '2' },
-          % else:
-          stickToTopPosition: function() { return vm.isPresentationMode() || vm.isResultFullScreenMode() ? 1 + bannerTopHeight : 76 + bannerTopHeight },
-          % endif
-          parentId: 'snippet_' + snippet.id(),
-          clonedContainerPosition: 'fixed',
-          app: 'editor'
-        });
-        $(el).jHueHorizontalScrollbar();
-      } else {
-        $(el).jHueTableExtender2({
-          mainScrollable: $(el).parents('.dataTables_wrapper')[0],
-          fixedFirstColumn: vm.editorMode(),
-          parentId: 'snippet_' + snippet.id(),
-          clonedContainerPosition: 'absolute',
-          app: 'editor'
-        });
-      }
-    }, 0);
-
-
-    return _dt;
-  }
-
-  function createDatatable(el, snippet, vm) {
-    var parent = $(el).parent();
-    // When executing few columns -> many columns -> few columns we have to clear the style
-    $(el).removeAttr('style');
-    if ($(el).hasClass('table-huedatatable')) {
-      $(el).removeClass('table-huedatatable');
-      if (parent.hasClass('dataTables_wrapper')) {
-        $(el).unwrap();
-      }
-    }
-    $(el).addClass("dt");
-
-    var _dt = createHueDatatable(el, snippet, vm);
-
-    var dataTableEl = $(el).parents(".dataTables_wrapper");
-
-    if (!vm.editorMode()) {
-      dataTableEl.bind('mousewheel DOMMouseScroll wheel', function (e) {
-        if ($(el).closest(".results").css("overflow") == "hidden") {
-          return;
-        }
-        var _e = e.originalEvent,
-            _deltaX = _e.wheelDeltaX || -_e.deltaX,
-            _deltaY = _e.wheelDeltaY || -_e.deltaY;
-        this.scrollTop += -_deltaY / 2;
-        this.scrollLeft += -_deltaX / 2;
-
-        if (this.scrollTop == 0) {
-          $("body")[0].scrollTop += -_deltaY / 3;
-          $("html")[0].scrollTop += -_deltaY / 3; // for firefox
-        }
-        e.preventDefault();
-      });
-    }
-
-    var _scrollTimeout = -1;
-
-    var scrollElement = dataTableEl;
-    if (vm.editorMode()) {
-      scrollElement = $(MAIN_SCROLLABLE);
-    }
-
-    if (scrollElement.data('scrollFnDtCreation')) {
-      scrollElement.off('scroll', scrollElement.data('scrollFnDtCreation'));
-    }
-
-    var resultFollowTimeout = -1;
-    var dataScroll = function () {
-      if (vm.editorMode()) {
-        var snippetEl = $('#snippet_' + snippet.id());
-        if (snippetEl.find('.dataTables_wrapper').length > 0 && snippet.showGrid()) {
-          window.clearTimeout(resultFollowTimeout);
-          resultFollowTimeout = window.setTimeout(function () {
-            var topCoord = vm.isPresentationMode() || vm.isResultFullScreenMode() ? 50 : 73;
-            var offsetTop = 0;
-            if (snippetEl.find('.dataTables_wrapper').length > 0 && snippetEl.find('.dataTables_wrapper').offset()){
-              offsetTop = (snippetEl.find('.dataTables_wrapper').offset().top - topCoord) * -1;
-            }
-            var margin = Math.max(offsetTop, 0);
-            %if conf.CUSTOM.BANNER_TOP_HTML.get():
-              margin += 31;
-            %endif
-            if (snippet.isResultSettingsVisible()) {
-              snippetEl.find('.snippet-grid-settings').css({
-                "height": vm.isPresentationMode() || !vm.editorMode() ? '330px' : Math.max(100, Math.ceil($(window).height() - Math.max($('#queryResults').offset().top, topCoord)))+ 'px'
-              });
-              snippetEl.find('.result-settings').css({
-                'marginTop': margin
-              });
-            }
-            snippetEl.find('.snippet-actions').css({
-              'marginTop': margin + 25
-            });
-          }, 100);
-        }
-      }
-      if (!vm.editorMode() || (vm.editorMode() && snippet.currentQueryTab() === 'queryResults' && snippet.showGrid())) {
-        var _lastScrollPosition = scrollElement.data("scrollPosition") != null ? scrollElement.data("scrollPosition") : 0;
-        window.clearTimeout(_scrollTimeout);
-        scrollElement.data("scrollPosition", scrollElement.scrollTop());
-        _scrollTimeout = window.setTimeout(function () {
-          if (vm.editorMode()) {
-            _lastScrollPosition--; //hack for forcing fetching
-          }
-          if (_lastScrollPosition != scrollElement.scrollTop() && scrollElement.scrollTop() + scrollElement.outerHeight() + 20 >= scrollElement[0].scrollHeight && _dt && snippet.result.hasMore()) {
-            huePubSub.publish('editor.snippet.result.gray', snippet);
-            snippet.fetchResult(100, false);
-          }
-        }, 100);
-      }
-    };
-    scrollElement.data('scrollFnDtCreation', dataScroll);
-    scrollElement.on('scroll', dataScroll);
-    snippet.isResultSettingsVisible.subscribe(function (newValue) {
-      if (newValue) {
-        dataScroll();
-      }
-    });
-
-    huePubSub.subscribeOnce('chart.hard.reset', function(){
-      // hard reset once the default opened chart
-      var oldChartX = snippet.chartX();
-      snippet.chartX(null);
-      window.setTimeout(function(){
-        snippet.chartX(oldChartX);
-      }, 0)
-    });
-
-    return _dt;
-  }
-
-  function toggleAllColumns(linkElement, snippet) {
-    var $t = $(linkElement).parents(".snippet").find("table.resultTable:eq(0)");
-    var dt = $t.hueDataTable();
-    dt.fnToggleAllCols(linkElement.checked);
-    dt.fnDraw();
-  }
-
-  function toggleColumn(linkElement, index, snippet) {
-    var $t = $(linkElement).parents(".snippet").find("table.resultTable:eq(0)");
-    var dt = $t.hueDataTable();
-    dt.fnSetColumnVis(index, linkElement.checked);
-  }
-
-  function scrollToColumn(linkElement, index) {
-    var $resultTable = $(linkElement).parents(".snippet").find("table.resultTable:eq(0)");
-    var _text = $.trim($(linkElement).text());
-    var _col = $resultTable.find("th").filter(function () {
-      return $.trim($(this).text()) == _text;
-    });
-    $resultTable.find(".columnSelected").removeClass("columnSelected");
-    var _colSel = $resultTable.find("tr th:nth-child(" + (_col.index() + 1) + ")");
-    if (_colSel.length > 0) {
-      $resultTable.find("tr td:nth-child(" + (_col.index() + 1) + ")").addClass("columnSelected");
-      $resultTable.parent().scrollLeft(_colSel.position().left + $resultTable.parent().scrollLeft() - $resultTable.parent().offset().left - 30);
-      $resultTable.data('scrollToCol', _col.index());
-      $resultTable.data('scrollToRow', null);
-      $resultTable.data('scrollAnimate', true);
-      $resultTable.parent().trigger('scroll');
-    }
-  }
-
-  function isNotNullForCharts(val) {
-    return val !== 'NULL' && val !== null;
-  }
-
-  function editorPieChartDataTransformer(rawDatum) {
-    var _data = [];
-
-    if (rawDatum.snippet.chartX() != null && rawDatum.snippet.chartYSingle() != null) {
-      var _idxValue = -1;
-      var _idxLabel = -1;
-      rawDatum.snippet.result.meta().forEach(function (col, idx) {
-        if (col.name == rawDatum.snippet.chartX()) {
-          _idxLabel = idx;
-        }
-        if (col.name == rawDatum.snippet.chartYSingle()) {
-          _idxValue = idx;
-        }
-      });
-      var colors = HueColors.cuiD3Scale();
-      $(rawDatum.counts()).each(function (cnt, item) {
-        if (isNotNullForCharts(item[_idxValue])) {
-          var val = item[_idxValue] * 1;
-          if (isNaN(val)) {
-            val = 0;
-          }
-          _data.push({
-            label: hueUtils.html2text(item[_idxLabel]),
-            value: val,
-            color: colors[cnt % colors.length],
-            obj: item
-          });
-        }
-      });
-    }
-
-    if (rawDatum.sorting == "asc") {
-      _data.sort(function (a, b) {
-        return a.value - b.value
-      });
-    }
-    if (rawDatum.sorting == "desc") {
-      _data.sort(function (a, b) {
-        return b.value - a.value
-      });
-    }
-
-    if (rawDatum.snippet.chartLimit()) {
-      _data = _data.slice(0, rawDatum.snippet.chartLimit());
-    }
-
-    return _data;
-  }
-
-  function editorMapChartDataTransformer(rawDatum) {
-    var _data = [];
-    if (rawDatum.snippet.chartX() != null && rawDatum.snippet.chartYSingle() != null) {
-      var _idxRegion = -1;
-      var _idxValue = -1;
-      rawDatum.snippet.result.meta().forEach(function (col, idx) {
-        if (col.name == rawDatum.snippet.chartX()) {
-          _idxRegion = idx;
-        }
-        if (col.name == rawDatum.snippet.chartYSingle()) {
-          _idxValue = idx;
-        }
-      });
-
-      $(rawDatum.counts()).each(function (cnt, item) {
-        if (isNotNullForCharts(item[_idxValue]) && isNotNullForCharts(item[_idxRegion])) {
-          _data.push({
-            label: item[_idxRegion],
-            value: item[_idxValue],
-            obj: item
-          });
-        }
-      });
-    }
-
-    if (rawDatum.snippet.chartLimit()) {
-      _data = _data.slice(0, rawDatum.snippet.chartLimit());
-    }
-
-    return _data;
-  }
-
-  // The leaflet map can freeze the browser with numbers outside the map
-  var MIN_LAT = -90;
-  var MAX_LAT = 90;
-  var MIN_LNG = -180;
-  var MAX_LNG = 180;
-
-  function editorLeafletMapChartDataTransformer(rawDatum) {
-    var _data = [];
-    if (rawDatum.snippet.chartX() != null && rawDatum.snippet.chartYSingle() != null) {
-      var _idxLat = -1;
-      var _idxLng = -1;
-      var _idxLabel = -1;
-      var _idxHeat = -1;
-      rawDatum.snippet.result.meta().forEach(function (col, idx) {
-        if (col.name == rawDatum.snippet.chartX()) {
-          _idxLat = idx;
-        }
-        if (col.name == rawDatum.snippet.chartYSingle()) {
-          _idxLng = idx;
-        }
-        if (col.name == rawDatum.snippet.chartMapLabel()) {
-          _idxLabel = idx;
-        }
-        if (col.name == rawDatum.snippet.chartMapHeat()) {
-          _idxHeat = idx;
-        }
-      });
-      if (rawDatum.snippet.chartMapLabel() != null) {
-        $(rawDatum.counts()).each(function (cnt, item) {
-          if (isNotNullForCharts(item[_idxLat]) && isNotNullForCharts(item[_idxLng])) {
-            _data.push({
-              lat: Math.min(Math.max(MIN_LAT, item[_idxLat]), MAX_LAT),
-              lng: Math.min(Math.max(MIN_LNG, item[_idxLng]), MAX_LNG),
-              label: hueUtils.html2text(item[_idxLabel]),
-              isHeat: rawDatum.snippet.chartMapType() === 'heat',
-              intensity: _idxHeat > -1 ? (item[_idxHeat]*1 != NaN ? item[_idxHeat]*1 : null) : null,
-              obj: item
-            });
-          }
-        });
-      } else {
-        $(rawDatum.counts()).each(function (cnt, item) {
-          if (isNotNullForCharts(item[_idxLat]) && isNotNullForCharts(item[_idxLng])) {
-            _data.push({
-              lat: Math.min(Math.max(MIN_LAT, item[_idxLat]), MAX_LAT),
-              lng: Math.min(Math.max(MIN_LNG, item[_idxLng]), MAX_LNG),
-              isHeat: rawDatum.snippet.chartMapType() === 'heat',
-              intensity: _idxHeat > -1 ? (item[_idxHeat]*1 != NaN ? item[_idxHeat]*1 : null) : null,
-              obj: item
-            });
-          }
-        });
-      }
-    }
-
-    if (rawDatum.snippet.chartLimit()) {
-      _data = _data.slice(0, rawDatum.snippet.chartLimit());
-    }
-
-    return _data;
-  }
-
-
-  function editorTimelineChartDataTransformer(rawDatum) {
-    var _datum = [];
-    var _plottedSerie = 0;
-
-    rawDatum.snippet.result.meta().forEach(function (meta) {
-      if (rawDatum.snippet.chartYMulti().indexOf(meta.name) > -1) {
-        var col = meta.name;
-        var _idxValue = -1;
-        var _idxLabel = -1;
-        rawDatum.snippet.result.meta().forEach(function (icol, idx) {
-          if (icol.name == rawDatum.snippet.chartX()) {
-            _idxLabel = idx;
-          }
-          if (icol.name == col) {
-            _idxValue = idx;
-          }
-        });
-
-        if (_idxValue > -1) {
-          var _data = [];
-          var colors = HueColors.cuiD3Scale();
-          $(rawDatum.counts()).each(function (cnt, item) {
-            if (isNotNullForCharts(item[_idxLabel]) && isNotNullForCharts(item[_idxValue])) {
-              _data.push({
-                series: _plottedSerie,
-                x: new Date(moment(hueUtils.html2text(item[_idxLabel])).valueOf()),
-                y: item[_idxValue] * 1,
-                color: colors[_plottedSerie % colors.length],
-                obj: item
-              });
-            }
-          });
-          if (rawDatum.sorting == "asc") {
-            _data.sort(function (a, b) {
-              return a.y - b.y
-            });
-          }
-          if (rawDatum.sorting == "desc") {
-            _data.sort(function (a, b) {
-              return b.y - a.y
-            });
-          }
-          if (rawDatum.snippet.chartLimit()) {
-            _data = _data.slice(0, rawDatum.snippet.chartLimit() );
-          }
-          _datum.push({
-            key: col,
-            values: _data
-          });
-          _plottedSerie++;
-        }
-      }
-    });
-
-    return _datum;
-  }
-
-  function editorMultiSerieDataTransformer(rawDatum) {
-    var _datum = [];
-
-    if (rawDatum.snippet.chartX() != null && rawDatum.snippet.chartYMulti().length > 0) {
-      var _plottedSerie = 0;
-
-      if (typeof rawDatum.snippet.chartXPivot() !== 'undefined') {
-        var _idxValue = -1;
-        var _idxLabel = -1;
-        var _isXDate = false;
-
-        rawDatum.snippet.result.meta().forEach(function (icol, idx) {
-          if (icol.name == rawDatum.snippet.chartX()) {
-            _isXDate = icol.type.toUpperCase().indexOf('DATE') > -1;
-            _idxLabel = idx;
-          }
-          if (icol.name == rawDatum.snippet.chartYSingle()) {
-            _idxValue = idx;
-          }
-        });
-
-        rawDatum.snippet.result.meta().forEach(function (meta, cnt) {
-          if (rawDatum.snippet.chartXPivot() === meta.name) {
-            var _idxPivot = cnt;
-            var colors = HueColors.cuiD3Scale();
-            var pivotValues = $.map(rawDatum.counts(), function (p) {
-              return p[_idxPivot];
-            });
-            pivotValues = pivotValues.filter(function (item, pos) {
-              return pivotValues.indexOf(item) === pos;
-            });
-            pivotValues.forEach(function (val, pivotCnt) {
-              var _data = [];
-              $(rawDatum.counts()).each(function (cnt, item) {
-                if (item[_idxPivot] === val) {
-                  if (isNotNullForCharts(item[_idxValue]) && isNotNullForCharts(item[_idxLabel])) {
-                    _data.push({
-                      x: _isXDate ? moment(item[_idxLabel]) : hueUtils.html2text(item[_idxLabel]),
-                      y: item[_idxValue] * 1,
-                      color: colors[pivotCnt % colors.length],
-                      obj: item
-                    });
-                  }
-                }
-              });
-              _datum.push({
-                key: hueUtils.html2text(val),
-                values: _data
-              });
-            });
-          }
-        });
-
-        // fills in missing values
-        var longest = 0;
-        var allXValues = [];
-        _datum.forEach(function (d) {
-          d.values.forEach(function (val) {
-            if (allXValues.indexOf(val.x) === -1) {
-              allXValues.push(val.x);
-            }
-          });
-        });
-
-        _datum.forEach(function (d) {
-          allXValues.forEach(function (val) {
-            if (!d.values.some(function (item) {
-                  return item.x === val
-                })) {
-              var zeroObj = jQuery.extend({}, d.values[0]);
-              zeroObj.y = 0;
-              zeroObj.x = val;
-              d.values.push(zeroObj)
-            }
-          });
-          if (d.values.length > longest) {
-            longest = d.values.length;
-          }
-        });
-
-
-        // this is to avoid D3 js errors when the data the user is trying to display is bogus
-        if (allXValues.length < longest) {
-          _datum.forEach(function (d) {
-            for (var i = d.values.length; i < longest; i++) {
-              var zeroObj = jQuery.extend({}, d.values[0]);
-              zeroObj.y = 0;
-              zeroObj.x = '';
-              d.values.push(zeroObj)
-            }
-          });
-        }
-
-        if (rawDatum.snippet.chartLimit()) {
-          _datum = _datum.slice(0, rawDatum.snippet.chartLimit());
-        }
-
-        if (rawDatum.sorting == "desc") {
-          _datum.forEach(function (d) {
-            d.values.sort(function (a, b) {
-              if (a.x > b.x) return -1;
-              if (a.x < b.x) return 1;
-              return 0;
-            });
-          });
-        }
-        else {
-          _datum.forEach(function (d) {
-            d.values.sort(function (a, b) {
-              if (a.x > b.x) return 1;
-              if (a.x < b.x) return -1;
-              return 0;
-            });
-          });
-        }
-      }
-      else {
-        rawDatum.snippet.result.meta().forEach(function (meta) {
-          if (rawDatum.snippet.chartYMulti().indexOf(meta.name) > -1) {
-            var col = meta.name;
-            var _idxValue = -1;
-            var _idxLabel = -1;
-            var _isXDate = false;
-            rawDatum.snippet.result.meta().forEach(function (icol, idx) {
-              if (icol.name == rawDatum.snippet.chartX()) {
-                _isXDate = icol.type.toUpperCase().indexOf('DATE') > -1;
-                _idxLabel = idx;
-              }
-              if (icol.name == col) {
-                _idxValue = idx;
-              }
-            });
-
-            if (_idxValue > -1) {
-              var _data = [];
-              var colors = HueColors.cuiD3Scale();
-              $(rawDatum.counts()).each(function (cnt, item) {
-                if (isNotNullForCharts(item[_idxValue]) && isNotNullForCharts(item[_idxLabel])) {
-                  _data.push({
-                    series: _plottedSerie,
-                    x: _isXDate ? moment(item[_idxLabel]) : hueUtils.html2text(item[_idxLabel]),
-                    y: item[_idxValue] * 1,
-                    color: colors[cnt % colors.length],
-                    obj: item
-                  });
-                }
-              });
-              if (rawDatum.sorting == "asc") {
-                _data.sort(function (a, b) {
-                  return a.y - b.y
-                });
-              }
-              if (rawDatum.sorting == "desc") {
-                _data.sort(function (a, b) {
-                  return b.y - a.y
-                });
-              }
-              if (rawDatum.snippet.chartLimit()) {
-                _data = _data.slice(0, rawDatum.snippet.chartLimit());
-              }
-              _datum.push({
-                key: col,
-                values: _data
-              });
-              _plottedSerie++;
-            }
-          }
-        });
-      }
-    }
-    return _datum;
-  }
-
-  function editorScatterChartDataTransformer(rawDatum) {
-    var datum = {};
-
-    if (rawDatum.snippet.chartX() != null && rawDatum.snippet.chartYSingle() != null) {
-      var idxX = -1;
-      var idxY = -1;
-      var idxSize = -1;
-      var idxGroup = -1;
-      rawDatum.snippet.result.meta().forEach(function (icol, idx) {
-        if (icol.name == rawDatum.snippet.chartX()) {
-          idxX = idx;
-        }
-        if (icol.name == rawDatum.snippet.chartYSingle()) {
-          idxY = idx;
-        }
-        if (icol.name == rawDatum.snippet.chartScatterSize()) {
-          idxSize = idx;
-        }
-        if (icol.name == rawDatum.snippet.chartScatterGroup()) {
-          idxGroup = idx;
-        }
-      });
-
-      if (idxX > -1 && idxY > -1) {
-
-        function createAndAddToArray(key, item) {
-          if (!datum[key]) {
-            datum[key] = [];
-          }
-          if (isNotNullForCharts(item[idxX]) && isNotNullForCharts(item[idxY])) {
-            datum[key].push({
-              x: item[idxX],
-              y: item[idxY],
-              shape: 'circle',
-              size: idxSize > -1 ? item[idxSize] : 100,
-              obj: item
-            });
-          }
-        }
-
-        if (idxGroup > -1) {
-          $(rawDatum.counts()).each(function (cnt, item) {
-            createAndAddToArray(item[idxGroup], item)
-          });
-        }
-        else {
-          $(rawDatum.counts()).each(function (cnt, item) {
-            createAndAddToArray('distro', item)
-          });
-        }
-      }
-    }
-
-    var returndDatum = [];
-    Object.keys(datum).forEach(function (key) {
-      returndDatum.push({
-        key: key,
-        values: rawDatum.snippet.chartLimit() ? datum[key].slice(0, rawDatum.snippet.chartLimit()) : datum[key]
-      });
-    });
-
-    return returndDatum;
-  }
-
-  (function () {
-    if (ko.options) {
-      ko.options.deferUpdates = true;
-    }
-
-    var VIEW_MODEL_OPTIONS = $.extend(${ options_json | n,unicode,antixss }, {
-      huePubSubId: HUE_PUB_SUB_EDITOR_ID,
-      user: '${ user.username }',
-      userId: ${ user.id },
-      suffix: '${ suffix }',
-      assistAvailable: true,
-      autocompleteTimeout: AUTOCOMPLETE_TIMEOUT,
-      snippetViewSettings: {
-        default: {
-          placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-          aceMode: 'ace/mode/sql',
-          snippetIcon: 'fa-database',
-          sqlDialect: true
-        },
-        code: {
-          placeHolder: '${ _("Example: 1 + 1, or press CTRL + space") }',
-          snippetIcon: 'fa-code'
-        },
-        hive: {
-          placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-          aceMode: 'ace/mode/hive',
-          snippetImage: '${ static("beeswax/art/icon_beeswax_48.png") }',
-          sqlDialect: true
-        },
-        impala: {
-          placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-          aceMode: 'ace/mode/impala',
-          snippetImage: '${ static("impala/art/icon_impala_48.png") }',
-          sqlDialect: true
-        },
-        jar : {
-          snippetIcon: 'fa-file-archive-o '
-        },
-        mysql: {
-          placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-          aceMode: 'ace/mode/mysql',
-          snippetIcon: 'fa-database',
-          sqlDialect: true
-        },
-        mysqljdbc: {
-          placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-          aceMode: 'ace/mode/mysql',
-          snippetIcon: 'fa-database',
-          sqlDialect: true
-        },
-        oracle: {
-          placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-          aceMode: 'ace/mode/oracle',
-          snippetIcon: 'fa-database',
-          sqlDialect: true
-        },
-        pig: {
-          placeHolder: '${ _("Example: 1 + 1, or press CTRL + space") }',
-          aceMode: 'ace/mode/pig',
-          snippetImage: '${ static("pig/art/icon_pig_48.png") }'
-        },
-        postgresql: {
-          placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-          aceMode: 'ace/mode/pgsql',
-          snippetIcon: 'fa-database',
-          sqlDialect: true
-        },
-        solr: {
-          placeHolder: '${ _("Example: SELECT fieldA, FieldB FROM collectionname, or press CTRL + space") }',
-          aceMode: 'ace/mode/mysql',
-          snippetIcon: 'fa-database',
-          sqlDialect: true
-        },
-        kafkasql: {
-          placeHolder: '${ _("Example: SELECT fieldA, FieldB FROM collectionname, or press CTRL + space") }',
-          aceMode: 'ace/mode/mysql',
-          snippetIcon: 'fa-database',
-          sqlDialect: true
-        },
-        java : {
-          snippetIcon: 'fa-file-code-o'
-        },
-        py : {
-          snippetIcon: 'fa-file-code-o'
-        },
-        pyspark: {
-          placeHolder: '${ _("Example: 1 + 1, or press CTRL + space") }',
-          aceMode: 'ace/mode/python',
-          snippetImage: '${ static("spark/art/icon_spark_48.png") }'
-        },
-        r: {
-          placeHolder: '${ _("Example: 1 + 1, or press CTRL + space") }',
-          aceMode: 'ace/mode/r',
-          snippetImage: '${ static("spark/art/icon_spark_48.png") }'
-        },
-        scala: {
-          placeHolder: '${ _("Example: 1 + 1, or press CTRL + space") }',
-          aceMode: 'ace/mode/scala',
-          snippetImage: '${ static("spark/art/icon_spark_48.png") }'
-        },
-        spark: {
-          placeHolder: '${ _("Example: 1 + 1, or press CTRL + space") }',
-          aceMode: 'ace/mode/scala',
-          snippetImage: '${ static("spark/art/icon_spark_48.png") }'
-        },
-        spark2: {
-          snippetImage: '${ static("spark/art/icon_spark_48.png") }'
-        },
-        mapreduce: {
-          snippetIcon: 'fa-file-archive-o'
-        },
-        shell: {
-          snippetIcon: 'fa-terminal'
-        },
-        sqoop1: {
-          placeHolder: '${ _("Example: import  --connect jdbc:hsqldb:file:db.hsqldb --table TT --target-dir hdfs://localhost:8020/user/foo -m 1") }',
-          snippetImage: '${ static("sqoop/art/icon_sqoop_48.png") }'
-        },
-        distcp: {
-          snippetIcon: 'fa-files-o'
-        },
-        sqlite: {
-          placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
-          aceMode: 'ace/mode/sql',
-          snippetIcon: 'fa-database',
-          sqlDialect: true
-        },
-        text: {
-          placeHolder: '${ _('Type your text here') }',
-          aceMode: 'ace/mode/text',
-          snippetIcon: 'fa-header'
-        },
-        markdown: {
-          placeHolder: '${ _('Type your markdown here') }',
-          aceMode: 'ace/mode/markdown',
-          snippetIcon: 'fa-header'
-        }
-      }
-    });
-
-    var viewModel;
-
-    var importExternalNotebook = function (notebook) {
-      var currentNotebook = viewModel.selectedNotebook();
-      currentNotebook.name(notebook.name);
-      currentNotebook.description(notebook.description);
-      currentNotebook.selectedSnippet(notebook.selectedSnippet);
-      notebook.snippets.forEach(function(snippet){
-        var newSnippet = currentNotebook.addSnippet({
-          type: snippet.type,
-          result: {}
-        });
-        newSnippet.statement_raw(snippet.statement);
-      });
-      hideHoverMsg(viewModel);
-    };
-
-    window.importExternalNotebook = importExternalNotebook;
-
-    var hideFixedHeaders = function() {
-      $('.jHueTableExtenderClonedContainer').hide();
-      $('.jHueTableExtenderClonedContainerColumn').hide();
-      $('.jHueTableExtenderClonedContainerCell').hide();
-      $('.fixed-header-row').hide();
-      $('.fixed-first-cell').hide();
-      $('.fixed-first-column').hide();
-    };
-
-    window.hideFixedHeaders = hideFixedHeaders;
-
-    var redrawTimeout = -1;
-    var redrawFixedHeaders = function (timeout) {
-      var renderer = function() {
-        if (! viewModel.selectedNotebook()) {
-          return;
-        }
-        viewModel.selectedNotebook().snippets().forEach(function (snippet) {
-          if (snippet.result.meta().length > 0) {
-            var tableExtender = $("#snippet_" + snippet.id() + ' .resultTable').data('plugin_jHueTableExtender2');
-            if (typeof tableExtender !== 'undefined') {
-              tableExtender.repositionHeader();
-              tableExtender.drawLockedRows();
-            }
-            $(MAIN_SCROLLABLE).data('lastScroll', $(MAIN_SCROLLABLE).scrollTop());
-            $(MAIN_SCROLLABLE).trigger('scroll');
-          }
-        });
-        $(".jHueTableExtenderClonedContainer").show();
-        $(".jHueTableExtenderClonedContainerColumn").show();
-        $(".jHueTableExtenderClonedContainerCell").show();
-        $('.fixed-header-row').show();
-        $('.fixed-first-cell').show();
-        $('.fixed-first-column').show();
-      };
-
-      if (timeout){
-        window.clearTimeout(redrawTimeout);
-        redrawTimeout = window.setTimeout(renderer, timeout);
-      } else {
-        renderer();
-      }
-      %if not is_embeddable:
-      $(MAIN_SCROLLABLE).jHueScrollUp();
-      %endif
-    };
-
-    var splitDraggableTimeout = -1;
-    huePubSub.subscribe('split.draggable.position', function () {
-      window.clearTimeout(splitDraggableTimeout);
-      splitDraggableTimeout = window.setTimeout(function () {
-        redrawFixedHeaders(100);
-      }, 200);
-    }, HUE_PUB_SUB_EDITOR_ID);
-
-    huePubSub.subscribe('redraw.fixed.headers', function () {
-      hideFixedHeaders();
-      redrawFixedHeaders(200);
-    }, HUE_PUB_SUB_EDITOR_ID);
-
-    huePubSub.subscribe('app.gained.focus', function (app) {
-      if (app === 'editor') {
-        huePubSub.publish('redraw.fixed.headers');
-        huePubSub.publish('hue.scrollleft.show');
-        huePubSub.publish('active.snippet.type.changed', viewModel.editorType());
-      }
-    }, HUE_PUB_SUB_EDITOR_ID);
-
-    huePubSub.subscribe('show.saveToFile.modal', function () {
-      $('#saveToFileModal${ suffix }').modal('show');
-    }, HUE_PUB_SUB_EDITOR_ID);
-
-    huePubSub.subscribe('tab.switched', function (tab) {
-      if (tab !== 'queryResults') {
-        $('.hue-datatable-search').hide();
-      }
-      if (tab === 'queryHistory') {
-        hueUtils.waitForRendered($('#queryHistory .history-table'), function(el) { return el.is(':visible') }, function() {
-          viewModel.selectedNotebook().forceHistoryInitialHeight(true);
-          huePubSub.publish('editor.calculate.history.height');
-        });
-      }
-    }, HUE_PUB_SUB_EDITOR_ID);
-
-    huePubSub.subscribe('detach.scrolls', function (snippet) {
-      var scrollElement = $('#snippet_' + snippet.id()).find('.dataTables_wrapper');
-      if (viewModel.editorMode()) {
-        scrollElement = $(MAIN_SCROLLABLE);
-      }
-      if (scrollElement.data('scrollFnDt')) {
-        scrollElement.off('scroll', scrollElement.data('scrollFnDt'));
-      }
-    }, HUE_PUB_SUB_EDITOR_ID);
-
-    huePubSub.subscribe('editor.calculate.history.height', function () {
-      if (viewModel.editorMode() && (viewModel.selectedNotebook().historyInitialHeight() === 0 || viewModel.selectedNotebook().forceHistoryInitialHeight())) {
-        var h = $('#queryHistory .history-table').height();
-        if (h === 0) {
-          h = viewModel.selectedNotebook().history().length * 32;
-        }
-        viewModel.selectedNotebook().historyInitialHeight(h + 80); // add pagination too
-        viewModel.selectedNotebook().forceHistoryInitialHeight(false);
-      }
-    }, HUE_PUB_SUB_EDITOR_ID);
-
-    window.redrawFixedHeaders = redrawFixedHeaders;
-
-    function addAce(content, snippetType) {
-      var snip = viewModel.selectedNotebook().addSnippet({type: snippetType, result: {}}, true);
-      snip.statement_raw(content);
-      aceChecks++;
-      snip.checkForAce = window.setInterval(function () {
-        if (snip.ace()) {
-          window.clearInterval(snip.checkForAce);
-          aceChecks--;
-          if (aceChecks == 0) {
-            hideHoverMsg(viewModel);
-            redrawFixedHeaders(200);
-          }
-        }
-      }, 100);
-    }
-
-    function replaceAce(content) {
-      var snip = viewModel.selectedNotebook().snippets()[0];
-      if (snip) {
-        snip.statement_raw(content);
-        snip.result.statements_count(1);
-        snip.ace().setValue(content, 1);
-        snip.result.statement_range({
-          start: {
-            row: 0,
-            column: 0
-          },
-          end: {
-            row: 0,
-            column: 0
-          }
-        });
-        snip.ace()._emit('focus');
-      }
-      hideHoverMsg(viewModel);
-      redrawFixedHeaders(200);
-    }
-    window.replaceAce = replaceAce;
-
-    function addMarkdown (content) {
-      var snip = viewModel.selectedNotebook().addSnippet({type: "markdown", result: {}}, true);
-      snip.statement_raw(content);
-    }
-
-    function addPySpark (content) {
-      addAce(content, "pyspark");
-    }
-
-    function addSql (content) {
-      addAce(content, "hive");
-    }
-
-    function addScala (content) {
-      addAce(content, "spark");
-    }
-
-    function parseExternalJSON(raw) {
-      try {
-        if (viewModel.editorMode()){
-          replaceAce(raw);
-        } else {
-          var loaded = typeof raw == "string" ? JSON.parse(raw) : raw;
-          if (loaded.nbformat) { //ipython
-            var cells = [];
-            if (loaded.nbformat == 3) {
-              cells = loaded.worksheets[0].cells;
-            } else if (loaded.nbformat == 4) {
-              cells = loaded.cells;
-            }
-            cells.forEach(function (cell, cellCnt) {
-              window.setTimeout(function () {
-                if (cell.cell_type == "code") {
-                  if (loaded.nbformat == 3) {
-                    addPySpark($.isArray(cell.input) ? cell.input.join("") : cell.input);
-                  } else {
-                    addPySpark($.isArray(cell.source) ? cell.source.join("") : cell.source);
-                  }
-                }
-                if (cell.cell_type == "heading") {
-                  var heading = $.isArray(cell.source) ? cell.source.join("") : cell.source;
-                  if (cell.level == 1) {
-                    heading += "\n====================";
-                  } else if (cell.level == 2) {
-                    heading += "\n--------------------";
-                  } else {
-                    heading = "### " + heading;
-                  }
-                  addMarkdown(heading);
-                }
-                if (cell.cell_type == "markdown") {
-                  addMarkdown($.isArray(cell.source) ? cell.source.join("") : cell.source);
-                }
-                if (cellCnt == cells.length - 1 && aceChecks == 0) {
-                  hideHoverMsg(viewModel);
-                }
-              }, 10);
-            });
-          }
-
-          if (loaded.paragraphs) { //zeppelin
-            if (loaded.name) {
-              viewModel.selectedNotebook().name(loaded.name);
-            }
-            loaded.paragraphs.forEach(function (paragraph) {
-              if (paragraph.text) {
-                var content = paragraph.text.split("\n");
-                if (content[0].indexOf("%md") > -1) {
-                  content.shift();
-                  addMarkdown(content.join("\n"));
-                } else if (content[0].indexOf("%sql") > -1 || content[0].indexOf("%hive") > -1) {
-                  content.shift();
-                  addSql(content.join("\n"));
-                } else if (content[0].indexOf("%pyspark") > -1) {
-                  content.shift();
-                  addPySpark(content.join("\n"));
-                } else {
-                  if (content[0].indexOf("%spark") > -1) {
-                    content.shift();
-                  }
-                  addScala(content.join("\n"));
-                }
-              }
-            });
-          }
-        }
-      }
-      catch (e) {
-        hideHoverMsg(viewModel);
-        replaceAce(raw);
-      }
-    }
-
-    window.parseExternalJSON = parseExternalJSON;
-
-    // Drag and drop iPython / Zeppelin notebooks
-    if (window.FileReader) {
-
-      var aceChecks = 0;
-
-      function handleFileSelect (evt) {
-        evt.stopPropagation();
-        evt.preventDefault();
-        var dt = evt.dataTransfer;
-        var files = dt.files;
-        if (files.length > 0){
-          showHoverMsg();
-        } else {
-          hideHoverMsg(viewModel);
-        }
-
-        for (var i = 0, f; f = files[i]; i++) {
-          var reader = new FileReader();
-          reader.onload = (function (file) {
-            return function (e) {
-              $(".hoverText").html("<i class='fa fa-spinner fa-spin'></i>");
-              parseExternalJSON(e.target.result);
-            };
-          })(f);
-          reader.readAsText(f);
-        }
-      }
-
-      function handleDragOver (evt) {
-        evt.stopPropagation();
-        evt.preventDefault();
-        evt.dataTransfer.dropEffect = "copy";
+      hive: {
+        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
+        aceMode: 'ace/mode/hive',
+        snippetImage: '${ static("beeswax/art/icon_beeswax_48.png") }',
+        sqlDialect: true
+      },
+      impala: {
+        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
+        aceMode: 'ace/mode/impala',
+        snippetImage: '${ static("impala/art/icon_impala_48.png") }',
+        sqlDialect: true
+      },
+      jar : {
+        snippetIcon: 'fa-file-archive-o '
+      },
+      mysql: {
+        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
+        aceMode: 'ace/mode/mysql',
+        snippetIcon: 'fa-database',
+        sqlDialect: true
+      },
+      mysqljdbc: {
+        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
+        aceMode: 'ace/mode/mysql',
+        snippetIcon: 'fa-database',
+        sqlDialect: true
+      },
+      oracle: {
+        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
+        aceMode: 'ace/mode/oracle',
+        snippetIcon: 'fa-database',
+        sqlDialect: true
+      },
+      pig: {
+        placeHolder: '${ _("Example: 1 + 1, or press CTRL + space") }',
+        aceMode: 'ace/mode/pig',
+        snippetImage: '${ static("pig/art/icon_pig_48.png") }'
+      },
+      postgresql: {
+        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
+        aceMode: 'ace/mode/pgsql',
+        snippetIcon: 'fa-database',
+        sqlDialect: true
+      },
+      solr: {
+        placeHolder: '${ _("Example: SELECT fieldA, FieldB FROM collectionname, or press CTRL + space") }',
+        aceMode: 'ace/mode/mysql',
+        snippetIcon: 'fa-database',
+        sqlDialect: true
+      },
+      kafkasql: {
+        placeHolder: '${ _("Example: SELECT fieldA, FieldB FROM collectionname, or press CTRL + space") }',
+        aceMode: 'ace/mode/mysql',
+        snippetIcon: 'fa-database',
+        sqlDialect: true
+      },
+      java : {
+        snippetIcon: 'fa-file-code-o'
+      },
+      py : {
+        snippetIcon: 'fa-file-code-o'
+      },
+      pyspark: {
+        placeHolder: '${ _("Example: 1 + 1, or press CTRL + space") }',
+        aceMode: 'ace/mode/python',
+        snippetImage: '${ static("spark/art/icon_spark_48.png") }'
+      },
+      r: {
+        placeHolder: '${ _("Example: 1 + 1, or press CTRL + space") }',
+        aceMode: 'ace/mode/r',
+        snippetImage: '${ static("spark/art/icon_spark_48.png") }'
+      },
+      scala: {
+        placeHolder: '${ _("Example: 1 + 1, or press CTRL + space") }',
+        aceMode: 'ace/mode/scala',
+        snippetImage: '${ static("spark/art/icon_spark_48.png") }'
+      },
+      spark: {
+        placeHolder: '${ _("Example: 1 + 1, or press CTRL + space") }',
+        aceMode: 'ace/mode/scala',
+        snippetImage: '${ static("spark/art/icon_spark_48.png") }'
+      },
+      spark2: {
+        snippetImage: '${ static("spark/art/icon_spark_48.png") }'
+      },
+      mapreduce: {
+        snippetIcon: 'fa-file-archive-o'
+      },
+      shell: {
+        snippetIcon: 'fa-terminal'
+      },
+      sqoop1: {
+        placeHolder: '${ _("Example: import  --connect jdbc:hsqldb:file:db.hsqldb --table TT --target-dir hdfs://localhost:8020/user/foo -m 1") }',
+        snippetImage: '${ static("sqoop/art/icon_sqoop_48.png") }'
+      },
+      distcp: {
+        snippetIcon: 'fa-files-o'
+      },
+      sqlite: {
+        placeHolder: '${ _("Example: SELECT * FROM tablename, or press CTRL + space") }',
+        aceMode: 'ace/mode/sql',
+        snippetIcon: 'fa-database',
+        sqlDialect: true
+      },
+      text: {
+        placeHolder: '${ _('Type your text here') }',
+        aceMode: 'ace/mode/text',
+        snippetIcon: 'fa-header'
+      },
+      markdown: {
+        placeHolder: '${ _('Type your markdown here') }',
+        aceMode: 'ace/mode/markdown',
+        snippetIcon: 'fa-header'
       }
-
-      var dropZone = $('#${ bindableElement }')[0];
-      dropZone.addEventListener("dragenter", showHoverMsg, false);
-      dropZone.addEventListener("dragover", handleDragOver, false);
-      dropZone.addEventListener("drop", handleFileSelect, false);
-
-      var isDraggingOverText = false;
-
-      $('#${ bindableElement }').find(".hoverText").on("dragenter", function (e) {
-        e.preventDefault();
-        e.stopPropagation();
-        e.stopImmediatePropagation();
-        isDraggingOverText = true;
-      });
-
-      $('#${ bindableElement }').find(".hoverText").on("dragleave", function (e) {
-        e.preventDefault();
-        e.stopPropagation();
-        e.stopImmediatePropagation();
-        isDraggingOverText = false;
-      });
-
-      $('#${ bindableElement }').find(".hoverMsg").on("dragleave", function (e) {
-        if (!isDraggingOverText) {
-          hideHoverMsg(viewModel);
-        }
-      });
     }
+  });
 
+  window.EDITOR_ENABLE_QUERY_SCHEDULING = '${ ENABLE_QUERY_SCHEDULING.get() }' === 'True';
 
-    $(document).ready(function () {
-      % if ENABLE_QUERY_SCHEDULING.get():
-      viewModel = new EditorViewModel(${ editor_id or 'null' }, ${ notebooks_json | n,unicode }, VIEW_MODEL_OPTIONS, CoordinatorEditorViewModel, RunningCoordinatorModel);
-      % else:
-      viewModel = new EditorViewModel(${ editor_id or 'null' }, ${ notebooks_json | n,unicode }, VIEW_MODEL_OPTIONS);
-      % endif
-      ko.applyBindings(viewModel, $('#${ bindableElement }')[0]);
-      viewModel.init();
-
-      sqlWorkerHandler.registerWorkers();
-
-      if (viewModel.isOptimizerEnabled()) {
-        % if OPTIMIZER.AUTO_UPLOAD_QUERIES.get():
-        huePubSub.subscribe("editor.upload.query", function (query_id) {
-          viewModel.selectedNotebook().snippets()[0].uploadQuery(query_id);
-        }, HUE_PUB_SUB_EDITOR_ID);
-        % endif
-
-        % if OPTIMIZER.AUTO_UPLOAD_DDL.get():
-        huePubSub.subscribe('editor.upload.table.stats', function (options) {
-          viewModel.selectedNotebook().snippets()[0].uploadTableStats(options);
-        }, HUE_PUB_SUB_EDITOR_ID);
-        % endif
-
-        % if OPTIMIZER.QUERY_HISTORY_UPLOAD_LIMIT.get() != 0:
-        huePubSub.subscribe("editor.upload.history", function () {
-          viewModel.selectedNotebook().snippets()[0].uploadQueryHistory(5);
-        }, HUE_PUB_SUB_EDITOR_ID);
-        % endif
-      }
-
-      viewModel.selectedNotebook.subscribe(function (newVal) {
-        huePubSub.publish('selected.notebook.changed', newVal);
-      });
-
-      huePubSub.subscribe('get.selected.notebook', function () {
-        huePubSub.publish('set.selected.notebook', viewModel.selectedNotebook());
-      }, HUE_PUB_SUB_EDITOR_ID);
-
-      huePubSub.subscribe('left.assist.show', function () {
-        if (!viewModel.isLeftPanelVisible() && viewModel.assistAvailable()) {
-          viewModel.isLeftPanelVisible(true);
-        }
-      }, HUE_PUB_SUB_EDITOR_ID);
-
-      var wasResultFullScreenMode = false;
-      var isAssistAvailable = viewModel.assistAvailable();
-      var wasLeftPanelVisible = viewModel.isLeftPanelVisible();
-      var wasRightPanelVisible = viewModel.isRightPanelVisible();
-
-      function exitPlayerMode() {
-        if (! wasResultFullScreenMode) {
-          viewModel.selectedNotebook().isPresentationMode(false);
-        } else {
-          viewModel.isResultFullScreenMode(false);
-        }
-        wasResultFullScreenMode = false;
-      }
-
-      huePubSub.subscribe('editor.presentation.operate.toggle', function (value) {
-        viewModel.isEditing(! viewModel.isEditing());
-        if (value) {
-          $(".jHueNotify").remove();
-          isAssistAvailable = viewModel.assistAvailable();
-          wasLeftPanelVisible = viewModel.isLeftPanelVisible();
-          wasRightPanelVisible = viewModel.isRightPanelVisible();
-
-          if (wasResultFullScreenMode) {
-            huePubSub.publish('both.assists.hide', true);
-          } else {
-            huePubSub.publish('right.assist.hide', true);
-          }
-
-          viewModel.assistWithoutStorage(true);
-          viewModel.assistAvailable(false);
-          viewModel.isLeftPanelVisible(true);
-          viewModel.isRightPanelVisible(false);
-          window.setTimeout(function(){
-            viewModel.assistWithoutStorage(false);
-          }, 0);
-          $(".navigator").hide();
-          $(".add-snippet").hide();
-          % if conf.CUSTOM.BANNER_TOP_HTML.get():
-            $(".main-content").attr("style", "top: 31px!important");
-          % else:
-            $(".main-content").css("top", "1px");
-          % endif
-          redrawFixedHeaders(200);
-          $(window).bind("keydown", "esc", exitPlayerMode);
-        } else {
-          hideFixedHeaders();
-          huePubSub.publish('both.assists.show', true);
-          viewModel.assistWithoutStorage(true);
-          viewModel.isLeftPanelVisible(wasLeftPanelVisible);
-          viewModel.isRightPanelVisible(wasRightPanelVisible);
-          viewModel.assistAvailable(isAssistAvailable);
-          window.setTimeout(function(){
-            viewModel.assistWithoutStorage(false);
-          }, 0);
-          $(".navigator").show();
-          $(".add-snippet").show();
-          % if conf.CUSTOM.BANNER_TOP_HTML.get():
-          $(".main-content").css("top", "112px");
-          % else:
-          $(".main-content").css("top", "74px");
-          % endif
-          redrawFixedHeaders(200);
-          $(window).unbind("keydown", exitPlayerMode);
-        }
-      }, HUE_PUB_SUB_EDITOR_ID);
-
-      viewModel.isResultFullScreenMode.subscribe(function(newValue) {
-        wasResultFullScreenMode = newValue;
-        huePubSub.publish('editor.presentation.operate.toggle', newValue);
-      });
-
-      huePubSub.subscribe('assist.set.manual.visibility', function () {
-        wasLeftPanelVisible = viewModel.isLeftPanelVisible();
-        wasRightPanelVisible = viewModel.isRightPanelVisible();
-      }, HUE_PUB_SUB_EDITOR_ID);
-
-      viewModel.isLeftPanelVisible.subscribe(function (value) {
-        redrawFixedHeaders(200);
-      });
-
-      $(document).on("updateResultHeaders", function (e) {
-        hideFixedHeaders();
-        redrawFixedHeaders(200);
-      });
-
-      $(document).on("showAuthModal", function (e, data) {
-        viewModel.authSessionUsername('${ user.username }');
-        viewModel.authSessionPassword('');
-        viewModel.authSessionType(data['type']);
-        viewModel.authSessionCallback(data['callback']);
-        $("#authModal${ suffix }").modal("show");
-      });
-
-      $(document).on("hideHistoryModal", function (e) {
-        $("#clearHistoryModal${ suffix }").modal("hide");
-      });
-
-      huePubSub.subscribe('show.retry.modal', function (data) {
-        $('#retryModal${ suffix }').modal('show');
-      }, HUE_PUB_SUB_EDITOR_ID);
-
-      huePubSub.subscribe('hide.retry.modal', function (data) {
-        $('#retryModal${ suffix }').modal('hide');
-      }, HUE_PUB_SUB_EDITOR_ID);
-
-      // Close the notebook snippets when leaving the page
-      window.onbeforeunload = function (e) {
-        if (!viewModel.selectedNotebook().avoidClosing) {
-          viewModel.selectedNotebook().close();
-        }
-      };
-      $(window).data('beforeunload', window.onbeforeunload);
-
-      $(".preview-sample").css("right", (10 + hueUtils.scrollbarWidth()) + "px");
-
-      function saveKeyHandler() {
-        if (viewModel.canSave()) {
-          viewModel.saveNotebook();
-        }
-        else {
-          $('#saveAsModal${ suffix }').modal('show');
-        }
-      }
-
-      huePubSub.subscribe('open.link', function(link) {
-        $(window).unbind("keydown.editor");
-        if (link.indexOf("editor") >= 0) {
-          initKeydownBindings();
-        }
-      });
-
-      function initKeydownBindings() {
-        $(window).bind("keydown.editor", "ctrl+s alt+s meta+s", function (e) {
-          e.preventDefault();
-          saveKeyHandler();
-          return false;
-        });
-        $(window).bind("keydown.editor", "ctrl+shift+p alt+shift+p meta+shift+p", function (e) {
-          e.preventDefault();
-          huePubSub.publish('editor.presentation.toggle');
-          return false;
-        });
-        $(window).bind("keydown.editor", "ctrl+e alt+e meta+e", function (e) {
-          e.preventDefault();
-          newKeyHandler();
-          return false;
-        });
-      }
-
-      if (document.location.href.indexOf("editor") >= 0) {
-        initKeydownBindings();
-      }
-
-      huePubSub.subscribe('editor.presentation.toggle', function () {
-        viewModel.selectedNotebook().isPresentationMode(!viewModel.isPresentationMode());
-      }, HUE_PUB_SUB_EDITOR_ID);
-
-      huePubSub.subscribe('editor.save', saveKeyHandler, HUE_PUB_SUB_EDITOR_ID);
-
-      $(document).bind('keyup', function (e) {
-        if (e.keyCode == 191 && e.shiftKey && !$(e.target).is('input') && !$(e.target).is('textarea')) {
-          $('#helpModal${ suffix }').modal('show');
-        }
-
-        if (e.keyCode == 191 && !e.shiftKey && !$(e.target).is('input') && !$(e.target).is('textarea')) {
-          if (viewModel.editorMode() && viewModel.selectedNotebook().snippets()[0].currentQueryTab() == 'queryResults') {
-            e.preventDefault();
-            var $t = $("#snippet_" + viewModel.selectedNotebook().snippets()[0].id()).find(".resultTable");
-            $t.hueDataTable().fnShowSearch();
-            return false;
-          }
-        }
-      });
-
-      function newKeyHandler() {
-        if (!viewModel.editorMode()) {
-          viewModel.selectedNotebook().newSnippet();
-        }
-        else {
-          viewModel.newNotebook(viewModel.editorType(), null, viewModel.selectedNotebook() ? viewModel.selectedNotebook().snippets()[0].currentQueryTab() : null);
-        }
-      }
-
-      huePubSub.subscribe('editor.create.new', newKeyHandler, HUE_PUB_SUB_EDITOR_ID);
-
-      var initialResizePosition = 100;
-
-      function getDraggableOptions (minY) {
-        return {
-          axis: "y",
-          start: function (e, ui) {
-            initialResizePosition = ui.offset.top;
-          },
-          drag: function (e, ui) {
-            draggableHelper($(this), e, ui);
-            $(".jHueTableExtenderClonedContainer").hide();
-            $(".jHueTableExtenderClonedContainerColumn").hide();
-            $(".jHueTableExtenderClonedContainerCell").hide();
-            $('.fixed-header-row').hide();
-            $('.fixed-first-cell').hide();
-            $('.fixed-first-column').hide();
-          },
-          stop: function (e, ui) {
-            $(".jHueTableExtenderClonedContainer").show();
-            $(".jHueTableExtenderClonedContainerColum").show();
-            $(".jHueTableExtenderClonedContainerCell").show();
-            $('.fixed-header-row').show();
-            $('.fixed-first-cell').show();
-            $('.fixed-first-column').show();
-            draggableHelper($(this), e, ui, true);
-            redrawFixedHeaders();
-            ui.helper.first().removeAttr("style");
-          },
-          containment: [0, minY, 4000, minY + 400]
-        }
-      }
-
-      $(".resize-panel a").each(function () {
-        $(this).draggable(getDraggableOptions($(this).parents(".snippet").offset().top + 128));
-      });
-
-      function draggableHelper (el, e, ui, setSize) {
-        var _snippet = ko.dataFor(el.parents(".snippet")[0]);
-        var _cm = $("#snippet_" + _snippet.id()).data("editor");
-        var _newSize = _snippet.aceSize() + (ui.offset.top - initialResizePosition);
-        _cm.setSize("99%", _newSize);
-        if (setSize) {
-          _snippet.aceSize(_newSize);
-        }
-      }
-
-      function resetResultsResizer(snippet) {
-        $("#snippet_" + snippet.id()).find('.table-results .column-side').width(hueUtils.bootstrapRatios.span3() + '%').data('newWidth', hueUtils.bootstrapRatios.span3());
-        if (snippet.isResultSettingsVisible()){
-          $("#snippet_" + snippet.id()).find('.table-results .grid-side').data('newWidth', hueUtils.bootstrapRatios.span9()).width(hueUtils.bootstrapRatios.span9() + '%');
-        }
-        else {
-          $("#snippet_" + snippet.id()).find('.table-results .grid-side').data('newWidth', 100).width('100%');
-        }
-        $("#snippet_" + snippet.id()).find('.resize-bar').css('left', '');
-        try {
-          $("#snippet_" + snippet.id()).find('.resize-bar').draggable('destroy');
-        }
-        catch (e){}
-
-        var initialPosition = 0;
-
-        $("#snippet_" + snippet.id()).find('.resize-bar').draggable({
-          axis: "x",
-          containment: $("#snippet_" + snippet.id()).find('.table-results'),
-          create: function (event, ui) {
-            initialPosition = $("#snippet_" + snippet.id()).find('.resize-bar').position().left;
-            $("#snippet_" + snippet.id()).find('.table-results .column-side').data('newWidth', hueUtils.bootstrapRatios.span3());
-            $("#snippet_" + snippet.id()).find('.meta-filter').width($("#snippet_" + snippet.id()).find('.table-results .column-side').width() - 28)
-          },
-          drag: function (event, ui) {
-            if (initialPosition == 0){
-              initialPosition = $("#snippet_" + snippet.id()).find('.resize-bar').position().left;
-            }
-            ui.position.left = Math.max(150, ui.position.left);
-            var newSpan3Width = ui.position.left * hueUtils.bootstrapRatios.span3() / initialPosition;
-            var newSpan9Width = 100 - newSpan3Width - hueUtils.bootstrapRatios.margin();
-            $("#snippet_" + snippet.id()).find('.table-results .column-side').width(newSpan3Width + '%').data('newWidth', newSpan3Width);
-            $("#snippet_" + snippet.id()).find('.table-results .grid-side').width(newSpan9Width + '%').data('newWidth', newSpan9Width);
-            $("#snippet_" + snippet.id()).find('.meta-filter').width($("#snippet_" + snippet.id()).find('.table-results .column-side').width() - 28)
-          },
-          stop: function () {
-            redrawFixedHeaders();
-            huePubSub.publish('resize.leaflet.map');
-          }
-        });
-      }
-
-      $(document).on("toggleResultSettings", function (e, snippet) {
-        window.setTimeout(function () {
-          $('#snippet_' + snippet.id()).find('.chart').trigger("forceUpdate");
-          $('#snippet_' + snippet.id()).find('.snippet-grid-settings').scrollLeft(0);
-          if (snippet.isResultSettingsVisible()){
-            $("#snippet_" + snippet.id()).find('.table-results .grid-side').width((100 - $("#snippet_" + snippet.id()).find('.table-results .column-side').data('newWidth') - hueUtils.bootstrapRatios.margin()) + '%');
-          }
-          else {
-            $("#snippet_" + snippet.id()).find('.table-results .grid-side').width('100%');
-          }
-          redrawFixedHeaders();
-          $(window).trigger('resize');
-        }, 10)
-      });
-
-      $(document).on("editorSizeChanged", function () {
-        window.setTimeout(forceChartDraws, 50);
-      });
-
-      $(document).on("redrawResults", function () {
-        window.setTimeout(forceChartDraws, 50);
-      });
-
-      $(document).on("executeStarted", function (e, options) {
-        var $el = $("#snippet_" + options.snippet.id()).find(".resultTable");
-        if (options.vm.editorMode()) {
-          $('#queryResults').css({
-            height: $el.height() + 'px'
-          });
-        }
-        $el.data('scrollToCol', null);
-        $el.data('scrollToRow', null);
-        $("#snippet_" + options.snippet.id()).find(".progress-snippet").animate({
-          height: "3px"
-        }, 100);
-        if ($el.hasClass("dt")) {
-          $el.removeClass("dt");
-          $("#eT" + options.snippet.id() + "jHueTableExtenderClonedContainer").remove();
-          $("#eT" + options.snippet.id() + "jHueTableExtenderClonedContainerColumn").remove();
-          $("#eT" + options.snippet.id() + "jHueTableExtenderClonedContainerCell").remove();
-          if ($el.hueDataTable()) {
-            $el.hueDataTable().fnDestroy();
-          }
-          $el.find("thead tr").empty();
-          $el.data('lockedRows', {});
-        }
-      });
-
-      function resizeToggleResultSettings (snippet, initial) {
-        var _dtElement;
-        if (snippet.showGrid()) {
-          _dtElement = $("#snippet_" + snippet.id()).find(".dataTables_wrapper");
-          var topCoord = viewModel.isPresentationMode() || viewModel.isResultFullScreenMode() ? ${ conf.CUSTOM.BANNER_TOP_HTML.get() and '31' or '1' } : 73;
-          $("#snippet_" + snippet.id()).find(".snippet-grid-settings").css({
-            "height": viewModel.isPresentationMode() || !viewModel.editorMode() ? '330px' : Math.ceil($(window).height() - Math.max($('.result-settings').length > 0 ? $('.result-settings').offset().top : 0, topCoord)) + 'px'
-          });
-        } else {
-          _dtElement = $("#snippet_" + snippet.id()).find(".chart:visible");
-        }
-        if (_dtElement.length == 0) {
-          _dtElement = $("#snippet_" + snippet.id()).find(".table-results");
-        }
-        _dtElement.parents(".snippet-body").find(".toggle-result-settings").css({
-          "height": (_dtElement.height() - 30) + "px",
-          "line-height": (_dtElement.height() - 30) + "px"
-        });
-        if (initial) {
-          $('#snippet_' + snippet.id()).find('.result-settings').css({
-            'marginTop': 0
-          });
-          $('#snippet_' + snippet.id()).find('.snippet-actions').css({
-            'marginTop': 0
-          });
-          huePubSub.publish('resize.leaflet.map');
-        }
-      }
+  window.EDITOR_ID = ${ editor_id or 'null' };
 
-      huePubSub.subscribe('editor.render.data', function (options) {
-        var _el = $("#snippet_" + options.snippet.id()).find(".resultTable");
-        if (options.data.length > 0) {
-          window.setTimeout(function () {
-            var _dt;
-            if (options.initial) {
-              options.snippet.result.meta.notifySubscribers();
-              $("#snippet_" + options.snippet.id()).find("select").trigger("chosen:updated");
-              _dt = createDatatable(_el, options.snippet, viewModel);
-              resetResultsResizer(options.snippet);
-            } else {
-              _dt = _el.hueDataTable();
-            }
-            try {
-              _dt.fnAddData(options.data);
-            } catch (e) {}
-            var _dtElement = $("#snippet_" + options.snippet.id()).find(".dataTables_wrapper");
-            huePubSub.publish('editor.snippet.result.normal', options.snippet);
-            _dtElement.scrollTop(_dtElement.data("scrollPosition"));
-            redrawFixedHeaders();
-            resizeToggleResultSettings(options.snippet, options.initial);
-          }, 300);
-        } else {
-          huePubSub.publish('editor.snippet.result.normal', options.snippet);
-        }
-        $("#snippet_" + options.snippet.id()).find("select").trigger('chosen:updated');
-        $('#snippet_' + options.snippet.id()).find('.snippet-grid-settings').scrollLeft(0);
-      }, HUE_PUB_SUB_EDITOR_ID);
-
-      huePubSub.subscribe('editor.redraw.data', function (options) {
-        hueUtils.waitForRendered("#snippet_" + options.snippet.id() + " .resultTable", function (el) {
-          return el.is(':visible')
-        }, function () {
-          var $el = $("#snippet_" + options.snippet.id()).find(".resultTable");
-          var dt = createDatatable($el, options.snippet, viewModel);
-          dt.fnAddData(options.snippet.result.data());
-        });
-      }, HUE_PUB_SUB_EDITOR_ID);
-
-      huePubSub.subscribe('editor.snippet.result.gray', function (snippet) {
-        var $snippet = $("#snippet_" + snippet.id());
-        $snippet.find(".dataTables_wrapper .fixed-first-column").css({opacity: '0'});
-        $snippet.find(".dataTables_wrapper .fixed-header-row").css({opacity: '0'});
-        $snippet.find(".dataTables_wrapper .fixed-first-cell").css({opacity: '0'});
-        $snippet.find(".dataTables_wrapper .resultTable").css({opacity: '0.55'});
-      }, HUE_PUB_SUB_EDITOR_ID);
-
-      huePubSub.subscribe('editor.snippet.result.normal', function (snippet) {
-        var $snippet = $("#snippet_" + snippet.id());
-        $snippet.find(".dataTables_wrapper .fixed-first-column").css({opacity: '1'});
-        $snippet.find(".dataTables_wrapper .fixed-header-row").css({opacity: '1'});
-        $snippet.find(".dataTables_wrapper .fixed-first-cell").css({opacity: '1'});
-        $snippet.find(".dataTables_wrapper .resultTable").css({opacity: '1'});
-      }, HUE_PUB_SUB_EDITOR_ID);
-
-      $(document).on("renderDataError", function (e, options) {
-        huePubSub.publish('editor.snippet.result.normal', options.snippet);
-      });
-
-      $(document).on("progress", function (e, options) {
-        if (options.data == 100) {
-          window.setTimeout(function () {
-            $("#snippet_" + options.snippet.id()).find(".progress-snippet").animate({
-              height: "0"
-            }, 100, function () {
-              options.snippet.progress(0);
-              redrawFixedHeaders();
-            });
-          }, 2000);
-        }
-      });
+  window.NOTEBOOKS_JSON = ${ notebooks_json | n,unicode };
 
-      huePubSub.subscribe('render.jqcron', function(){
-        if (typeof renderJqCron !== 'undefined'){
-          renderJqCron();
-        }
-      }, HUE_PUB_SUB_EDITOR_ID);
-
-      huePubSub.subscribe('submit.popup.return', function (data) {
-        viewModel.selectedNotebook().viewSchedulerId(data.job_id);
-        $('.submit-modal-editor').modal('hide');
-        huePubSub.publish('show.jobs.panel', {id: data.job_id, interface: 'workflows'});
-      }, HUE_PUB_SUB_EDITOR_ID);
-
-      huePubSub.subscribe('jobbrowser.data', function (jobs) {
-        var snippet = viewModel.selectedNotebook().snippets()[0];
-        if (!snippet || snippet.type() === 'impala') {
-          return;
-        }
-        if (jobs.length > 0) {
-          var progress = 0;
-          var parent;
-          jobs.forEach(function (job) {
-            var id = job.shortId || job.id;
-            var el = $(".jobs-overlay li:contains(" + id + ')');
-            if (!el.length) {
-              return;
-            }
-            var context = ko.contextFor(el[0]);
-            parent = context.$parent;
-            var _job = context.$data;
-            progress = parseInt(job.mapsPercentComplete);
-            if (isNaN(progress)) {
-              progress = parseInt(job.progress);
-            }
-            if (!isNaN(progress)) {
-              _job.percentJob(progress);
-            } else {
-              progress = 0;
-            }
-          });
-          if (parent && parent.jobs().length == 1) {
-            parent.progress(Math.max(progress, parent.progress()));
-          }
-        }
-      }, HUE_PUB_SUB_EDITOR_ID);
-
-      huePubSub.subscribe('editor.get.active.risks', function (callback) {
-        var result = {
-          editor: undefined,
-          risks : {}
-        };
-        if (viewModel.selectedNotebook()) {
-          if (viewModel.selectedNotebook().snippets().length === 1) {
-            result.editor = viewModel.selectedNotebook().snippets()[0].ace();
-            result.risks = viewModel.selectedNotebook().snippets()[0].complexity() || {};
-          } else {
-            var notFound = viewModel.selectedNotebook().snippets().every(function (snippet) {
-              if (snippet.inFocus()) {
-                result.editor = snippet.ace();
-                result.risks = snippet.complexity() || {};
-                return false;
-              }
-              return true;
-            });
-          }
-        }
-        callback(result);
-      }, HUE_PUB_SUB_EDITOR_ID);
-
-      huePubSub.subscribe('editor.grid.shown', function (snippet) {
-        hueUtils.waitForRendered('#snippet_' + snippet.id() + ' .dataTables_wrapper', function (el) {
-          return el.is(':visible')
-        }, function () {
-          resizeToggleResultSettings(snippet, true);
-          forceChartDraws();
-          $('#snippet_' + snippet.id()).find('.snippet-grid-settings').scrollLeft(0);
-        });
-      }, HUE_PUB_SUB_EDITOR_ID);
-
-      huePubSub.subscribe('editor.chart.shown', function (snippet) {
-        resizeToggleResultSettings(snippet, true);
-      }, HUE_PUB_SUB_EDITOR_ID);
-
-      $(document).on("forceChartDraw", function (e, snippet) {
-        window.setTimeout(function () {
-          snippet.chartX.notifySubscribers();
-          snippet.chartX.valueHasMutated();
-        }, 100);
-      });
-
-      var hideTimeout = -1;
-      $(document).on("hideAutocomplete", function () {
-        window.clearTimeout(hideTimeout);
-        hideTimeout = window.setTimeout(function () {
-          var $aceAutocomplete = $(".ace_editor.ace_autocomplete");
-          if ($aceAutocomplete.is(":visible")) {
-            $aceAutocomplete.hide();
-          }
-        }, 100);
-      });
-
-      function forceChartDraws(initial) {
-        if (viewModel.selectedNotebook()) {
-          viewModel.selectedNotebook().snippets().forEach(function (snippet) {
-            if (snippet.result.data().length > 0) {
-              var _elCheckerInterval = -1;
-              var _el = $("#snippet_" + snippet.id());
-              _elCheckerInterval = window.setInterval(function () {
-                if (_el.find(".resultTable").length > 0) {
-                  try {
-                    resizeToggleResultSettings(snippet, initial);
-                    resetResultsResizer(snippet);
-                    $(document).trigger("forceChartDraw", snippet);
-                  } catch (e) { }
-                  window.clearInterval(_elCheckerInterval);
-                }
-              }, 200)
-            }
-          });
-        }
-      }
+  window.OPTIMIZER_AUTO_UPLOAD_QUERIES = '${ OPTIMIZER.AUTO_UPLOAD_QUERIES.get() }' === 'True';
 
-      forceChartDraws(true);
+  window.OPTIMIZER_AUTO_UPLOAD_DDL = '${ OPTIMIZER.AUTO_UPLOAD_DDL.get() }' === 'True';
 
-      huePubSub.subscribe('recalculate.name.description.width', function () {
-        hueUtils.waitForRendered('.editorComponents .hue-title-bar .query-name', function(el){ return el.is(':visible') }, function(){
-          var cumulativeWidth = 0;
-          $('.editorComponents .hue-title-bar ul li:not(.skip-width-calculation)').each(function(){
-            cumulativeWidth += $(this).outerWidth();
-          });
-          $('.notebook-name-desc').css('max-width', (($('.editorComponents .hue-title-bar').width() - cumulativeWidth - $('.editorComponents .hue-title-bar .pull-right').width() - 120)/2) + 'px');
-        });
-      }, HUE_PUB_SUB_EDITOR_ID);
-
-      var _resizeTimeout = -1;
-      $(window).on("resize", function () {
-        huePubSub.publish('recalculate.name.description.width');
-        window.clearTimeout(_resizeTimeout);
-        _resizeTimeout = window.setTimeout(function () {
-          forceChartDraws();
-        }, 200);
-      });
-    });
-  })();
+  window.OPTIMIZER_QUERY_HISTORY_UPLOAD_LIMIT = ${ OPTIMIZER.QUERY_HISTORY_UPLOAD_LIMIT.get() };
 </script>
 
 </%def>

+ 20 - 0
package-lock.json

@@ -7303,6 +7303,26 @@
         "object-visit": "^1.0.0"
       }
     },
+    "markdown": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/markdown/-/markdown-0.5.0.tgz",
+      "integrity": "sha1-KCBbVlqK51kt4gdGPWY33BgnIrI=",
+      "dev": true,
+      "requires": {
+        "nopt": "~2.1.1"
+      },
+      "dependencies": {
+        "nopt": {
+          "version": "2.1.2",
+          "resolved": "https://registry.npmjs.org/nopt/-/nopt-2.1.2.tgz",
+          "integrity": "sha1-bMzZd7gBMqB3MdbozljCyDA8+a8=",
+          "dev": true,
+          "requires": {
+            "abbrev": "1"
+          }
+        }
+      }
+    },
     "markdown-escapes": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.2.tgz",

+ 1 - 0
package.json

@@ -79,6 +79,7 @@
     "karma-spec-reporter": "0.0.32",
     "karma-webpack": "4.0.0-rc.6",
     "lesshint": "6.3.2",
+    "markdown": "0.5.0",
     "load-grunt-tasks": "4.0.0",
     "prettier": "1.16.1",
     "stylelint": "^9.10.1",

+ 1 - 1
webpack.config.js

@@ -23,7 +23,7 @@ module.exports = {
   },
   entry: {
     hue: ['./desktop/core/src/desktop/js/hue.js'],
-    notebook: ['./desktop/core/src/desktop/js/apps/notebook/notebook.js']
+    notebook: ['./desktop/core/src/desktop/js/apps/notebook/app.js']
   },
   optimization: {
     minimize: true,

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