浏览代码

[core] Extended support for AceEditor

Created Hue theme
Added error handling
Added support for Hive/Impala autocomplete
Added support for HDFS file picker
Enrico Berti 10 年之前
父节点
当前提交
409f849190

+ 209 - 0
apps/spark/src/spark/templates/ace.mako

@@ -0,0 +1,209 @@
+## Licensed to Cloudera, Inc. under one
+## or more contributor license agreements.  See the NOTICE file
+## distributed with this work for additional information
+## regarding copyright ownership.  Cloudera, Inc. licenses this file
+## to you under the Apache License, Version 2.0 (the
+## "License"); you may not use this file except in compliance
+## with the License.  You may obtain a copy of the License at
+##
+##     http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+
+<%!
+  from desktop.views import commonheader, commonfooter, commonshare, commonimportexport
+  from django.utils.translation import ugettext as _
+%>
+<%namespace name="actionbar" file="actionbar.mako" />
+
+${ commonheader(_("Notebooks"), "spark", user, "60px") | n,unicode }
+
+<script src="${ static('desktop/ext/js/ace/ace.js') }"></script>
+<script src="${ static('desktop/ext/js/ace/ext-language_tools.js') }"></script>
+<script src="${ static('desktop/js/ace.extended.js') }"></script>
+
+<style type="text/css" media="screen">
+  .editor {
+    height: 200px;
+    margin: 20px;
+  }
+  .filechooser {
+    position: absolute;
+    display: none;
+    z-index: 20000;
+    background-color: #FFFFFF;
+    padding: 10px;
+    min-width: 350px;
+  }
+</style>
+
+<!-- ko foreach: editors -->
+  <div class="editor" data-bind="attr: {id: UUID()}, aceEditor: {value: snippet, ace: ace, mode: mode, onChange: changeEditor, onAfterExec: textInputHandler, extraCompleters: completers, errors: errors }">
+  </div>
+<!-- /ko -->
+
+<div class="filechooser">
+  <a class="pointer pull-right" data-bind="click: function(){ $('.filechooser').hide(); }"><i class="fa fa-times"></i></a>
+  <div class="filechooser-content">
+    <!--[if !IE]><!--><i class="fa fa-spinner fa-spin" style="font-size: 30px; color: #DDD"></i><!--<![endif]-->
+    <!--[if IE]><img src="${ static('desktop/art/spinner.gif') }"/><![endif]-->
+  </div>
+</div>
+
+<script src="${ static('desktop/ext/js/knockout.min.js') }" type="text/javascript" charset="utf-8"></script>
+<script src="${ static('desktop/ext/js/knockout-mapping.min.js') }" type="text/javascript" charset="utf-8"></script>
+<script src="${ static('desktop/ext/js/knockout-sortable.min.js') }" type="text/javascript" charset="utf-8"></script>
+<script src="${ static('desktop/ext/js/knockout-deferred-updates.min.js') }" type="text/javascript" charset="utf-8"></script>
+<script src="${ static('desktop/js/ko.hue-bindings.js') }" type="text/javascript" charset="utf-8"></script>
+<script src="${ static('beeswax/js/autocomplete.utils.js') }" type="text/javascript" charset="utf-8"></script>
+
+<script>
+
+  var HIVE_AUTOCOMPLETE_BASE_URL = "/beeswax/api/autocomplete/";
+  var HIVE_AUTOCOMPLETE_FAILS_QUIETLY_ON = [500]; // error codes from beeswax/views.py - autocomplete
+  var HIVE_AUTOCOMPLETE_USER = "hue";
+  var HIVE_AUTOCOMPLETE_APP = "beeswax";
+
+  var STATS_PROBLEMS = "${ _('There was a problem loading the stats.') }";
+
+  var HIVE_AUTOCOMPLETE_GLOBAL_CALLBACK = function (data) {
+    if (data != null && data.error && typeof resetNavigator != "undefined") {
+      resetNavigator();
+    }
+  };
+
+  function newCompleter(items) {
+    return {
+      getCompletions: function(editor, session, pos, prefix, callback) {
+        callback(null, items);
+      }
+    }
+  }
+
+  function textInputHandler(event, editor, valueAccessor) {
+
+  }
+
+  function changeEditor(event, editor, valueAccessor) {
+    // if it's pig and before it's LOAD ' we disable the autocomplete and show a filechooser btn
+    if (editor.session.getMode().$id = "ace/mode/pig") {
+
+    }
+    else {
+
+      if (event.data.text == "."){
+      editor.showSpinner();
+      // fill up with fields
+      hac_getTableColumns("default", "sample_07", editor.getValue(), function(data){
+        var _fieldNames = data.split(" ");
+        var _fields = [];
+        _fieldNames.forEach(function(fld){
+          _fields.push({value: fld, score: 1000, meta: "column"});
+        });
+        valueAccessor().extraCompleters([newCompleter(_fields)]);
+        editor.hideSpinner();
+      })
+    }
+    else {
+      editor.showSpinner();
+      valueAccessor().extraCompleters([]);
+      hac_getTables("default", function(data){
+        var _tableNames = data.split(" ");
+        var _tables = [];
+        _tableNames.forEach(function(tbl){
+          _tables.push({value: tbl, score: 1000, meta: "table"});
+        });
+        valueAccessor().extraCompleters([newCompleter(_tables)]);
+        editor.hideSpinner();
+      });
+    }
+
+    }
+  }
+
+  function Snip(mode) {
+    var self = this;
+    self.ace = ko.observable(null);
+    self.mode = ko.observable(mode);
+    self.snippet = ko.observable("");
+    self.completers = ko.observableArray([]);
+    self.errors = ko.observableArray([]);
+  }
+
+  function AceViewModel() {
+    var self = this;
+    self.editors = ko.observableArray();
+  }
+
+  function addEditor() {
+##    var snippy2 = new Snip("ace/mode/python");
+##    snippy2.snippet("def nano:");
+##    viewModel.editors.push(snippy2);
+##    var snippy3 = new Snip("ace/mode/hivesql");
+##    snippy3.snippet("SELECT * FROM sample_07");
+##    viewModel.editors.push(snippy3);
+    var snippy = new Snip("ace/mode/pig");
+    snippy.snippet("A = LOAD ''");
+    viewModel.editors.push(snippy);
+  }
+
+  var viewModel = new AceViewModel();
+  ko.applyBindings(viewModel);
+  addEditor();
+
+  var extra = {
+    getCompletions: function(editor, session, pos, prefix, callback) {
+      callback(null, [
+        {value: "sample_07", score: 1000, meta: "table"},
+        {value: "code", score: 1000, meta: "column"},
+        {value: "ud", score: 1000, meta: "column"}
+      ]);
+    }
+  }
+
+
+##
+##  editor.completers.push({
+##    getCompletions: function(editor, session, pos, prefix, callback) {
+##      callback(null, [
+##        {value: "default", score: 1000, meta: "database"},
+##        {value: "sample_07", score: 1000, meta: "table"},
+##        {value: "code", score: 1000, meta: "column"}
+##      ]);
+##    }
+##  })
+##
+##          var snippetManager = ace.require("ace/snippets").snippetManager;
+##        var config = ace.require("ace/config");
+##
+##      ace.config.loadModule("ace/snippets/python", function(m) {
+##        if (m) {
+##          snippetManager.files.python = m;
+##          m.snippets = snippetManager.parseSnippetFile(m.snippetText);
+##
+##          // or do this if you already have them parsed
+##          m.snippets.push({
+##            content: "SELECT * FROM sample_07",
+##            name: "SELECT * FROM sample_07",
+##            tabTrigger: "sel07"
+##          });
+##          m.snippets.push({
+##            content: "SELECT * FROM sample_08",
+##            name: "SELECT * FROM sample_08",
+##            tabTrigger: "sel08"
+##          });
+##          snippetManager.register(m.snippets, m.scope);
+##          }
+##        });
+##
+##
+
+
+</script>
+
+
+${commonfooter(messages) | n,unicode}

+ 3 - 0
apps/spark/src/spark/views.py

@@ -112,6 +112,9 @@ def notebooks(request):
       'notebooks_json': json.dumps(notebooks, cls=JSONEncoderForHTML)
   })
 
+def ace(request):
+  return render('ace.mako', request, {})
+
 
 @check_document_modify_permission()
 def delete(request):

+ 0 - 1
desktop/core/src/desktop/static/desktop/ext/js/ace/ext-themelist.js

@@ -2,4 +2,3 @@ ace.define("ace/ext/themelist",["require","exports","module","ace/lib/fixoldbrow
                 (function() {
                     ace.require(["ace/ext/themelist"], function() {});
                 })();
-            

+ 138 - 0
desktop/core/src/desktop/static/desktop/ext/js/ace/theme-hue.js

@@ -0,0 +1,138 @@
+// 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.
+
+ace.define("ace/theme/hue",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
+
+exports.isDark = false;
+exports.cssClass = "ace-hue";
+exports.cssText = ".ace-hue .ace_gutter {\
+background: #f6f6f6;\
+color: #4D4D4C\
+}\
+.ace-hue .ace_print-margin {\
+width: 1px;\
+background: #f6f6f6\
+}\
+.ace-hue {\
+background-color: #FFFFFF;\
+color: #4D4D4C\
+}\
+.ace-hue .ace_cursor {\
+color: #AEAFAD\
+}\
+.ace-hue .ace_marker-layer .ace_selection {\
+background: #D6D6D6\
+}\
+.ace-tomorrow.ace_multiselect .ace_selection.ace_start {\
+box-shadow: 0 0 3px 0px #FFFFFF;\
+border-radius: 2px\
+}\
+.ace-hue .ace_marker-layer .ace_step {\
+background: rgb(255, 255, 0)\
+}\
+.ace-hue .ace_marker-layer .ace_bracket {\
+margin: -1px 0 0 -1px;\
+border: 1px solid #D1D1D1\
+}\
+.ace-hue .ace_marker-layer .ace_active-line {\
+background: #EFEFEF\
+}\
+.ace-hue .ace_marker-layer .ace_error-line {\
+background: #F2DEDE\
+}\
+.ace-hue .ace_gutter-active-line {\
+background-color : #dcdcdc\
+}\
+.ace-hue .ace_marker-layer .ace_selected-word {\
+border: 1px solid #D6D6D6\
+}\
+.ace-hue .ace_invisible {\
+color: #D1D1D1\
+}\
+.ace-hue .ace_keyword,\
+.ace-hue .ace_meta,\
+.ace-hue .ace_storage,\
+.ace-hue .ace_storage.ace_type,\
+.ace-hue .ace_support.ace_type {\
+color: #8959A8\
+}\
+.ace-hue .ace_keyword.ace_operator {\
+color: #3E999F\
+}\
+.ace-hue .ace_constant.ace_character,\
+.ace-hue .ace_constant.ace_language,\
+.ace-hue .ace_constant.ace_numeric,\
+.ace-hue .ace_keyword.ace_other.ace_unit,\
+.ace-hue .ace_support.ace_constant,\
+.ace-hue .ace_variable.ace_parameter {\
+color: #F5871F\
+}\
+.ace-hue .ace_constant.ace_other {\
+color: #666969\
+}\
+.ace-hue .ace_invalid {\
+color: #FFFFFF;\
+background-color: #C82829\
+}\
+.ace-hue .ace_invalid.ace_deprecated {\
+color: #FFFFFF;\
+background-color: #8959A8\
+}\
+.ace-hue .ace_fold {\
+background-color: #4271AE;\
+border-color: #4D4D4C\
+}\
+.ace-hue .ace_entity.ace_name.ace_function,\
+.ace-hue .ace_support.ace_function,\
+.ace-hue .ace_variable {\
+color: #4271AE\
+}\
+.ace-hue .ace_support.ace_class,\
+.ace-hue .ace_support.ace_type {\
+color: #C99E00\
+}\
+.ace-hue .ace_heading,\
+.ace-hue .ace_markup.ace_heading,\
+.ace-hue .ace_string {\
+color: #718C00\
+}\
+.ace-hue .ace_entity.ace_name.ace_tag,\
+.ace-hue .ace_entity.ace_other.ace_attribute-name,\
+.ace-hue .ace_meta.ace_tag,\
+.ace-hue .ace_string.ace_regexp,\
+.ace-hue .ace_variable {\
+color: #C82829\
+}\
+.ace-hue .ace_comment {\
+color: #8E908C\
+}\
+.ace-hue .ace_indent-guide {\
+background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bdu3f/BwAlfgctduB85QAAAABJRU5ErkJggg==) right repeat-y\
+}\
+.ace-spinner, .ace-inline-button {\
+  position: absolute;\
+  z-index: 1030;\
+}\
+.ace-inline-button {\
+  opacity: 0.7;\
+}\
+.ace-inline-button:hover {\
+  opacity: 1;\
+}";
+
+var dom = require("../lib/dom");
+dom.importCssString(exports.cssText, exports.cssClass);
+});

+ 93 - 0
desktop/core/src/desktop/static/desktop/js/ace.extended.js

@@ -0,0 +1,93 @@
+// 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.
+
+try {
+  ace.originalEdit = ace.edit;
+  var AceRange = ace.require('ace/range').Range;
+  ace.edit = function(el) {
+    var editor = ace.originalEdit(el);
+
+    editor.enableAutocomplete = function() {
+      editor.setOptions({enableBasicAutocompletion: true, enableLiveAutocompletion: true});
+    }
+
+    editor.disableAutocomplete = function() {
+      editor.setOptions({enableBasicAutocompletion: false, enableLiveAutocompletion: false});
+    }
+
+    editor.getTextBeforeCursor = function (separator) {
+      var _r = new AceRange(0, 0, this.getCursorPosition().row, this.getCursorPosition().column);
+      return this.session.getTextRange(_r);
+    }
+
+    editor.getCursorScreenPosition = function () {
+      return this.renderer.textToScreenCoordinates(this.getCursorPosition());
+    }
+
+    editor.showSpinner = function () {
+      var _position = this.getCursorScreenPosition();
+      if ($(".ace-spinner").length == 0) {
+        $("<i class='fa fa-spinner fa-spin ace-spinner'></i>").appendTo($("body"));
+      }
+      $(".ace-spinner").css("top", _position.pageY + "px").css("left", (_position.pageX - 4) + "px").show();
+    }
+
+    editor.hideSpinner = function () {
+      $(".ace-spinner").hide();
+    }
+
+    editor.showFileButton = function () {
+      var _position = this.getCursorScreenPosition();
+      if ($(".ace-inline-button").length == 0) {
+        $("<a class='btn btn-mini ace-inline-button'><i class='fa fa-ellipsis-h'></i></a>").appendTo($("body"));
+      }
+      $(".ace-inline-button").css("top", _position.pageY + "px").css("left", (_position.pageX + 4) + "px").show();
+      $(".ace-inline-button").off("click");
+      return $(".ace-inline-button");
+    }
+
+    editor.hideFileButton = function () {
+      $(".ace-inline-button").hide();
+    }
+
+    editor.clearErrors = function() {
+      for (var id in this.session.getMarkers()) {
+        var _marker = this.session.getMarkers()[id];
+        if (_marker.clazz == "ace_error-line"){
+          this.session.removeMarker(_marker.id);
+        }
+      };
+      this.session.clearAnnotations();
+    }
+
+    editor.addError = function (message, line) {
+      var _range = new AceRange(line, 0, line, this.session.getLine(line).length);
+      this.session.addMarker(_range, "ace_error-line");
+      this.session.setAnnotations([{
+        row: _range.start.row,
+        column: _range.start.column,
+        raw: message,
+        text: message,
+        type: "error"
+      }]);
+    }
+
+    return editor;
+  }
+}
+catch (e) {
+  console.error("You need to include ace.js before including this snippet.")
+}

+ 82 - 15
desktop/core/src/desktop/static/desktop/js/ko.hue-bindings.js

@@ -1488,35 +1488,102 @@ ko.bindingHandlers.aceEditor = {
       var _options = ko.unwrap(valueAccessor());
       var _onBlur = _options.onBlur || function () {};
       var _onChange = _options.onChange || function () {};
+      var _onAfterExec = _options.onAfterExec || function () {};
       _el.text(_options.value());
-      ace.require("ace/ext/language_tools");
-      var editor = ace.edit(element);
+      var editor = ace.edit(_el.attr("id"));
       editor.session.setMode(_options.mode());
-      editor.setTheme($.totalStorage("hue.ace.theme") || "ace/theme/clouds");
-      editor.setOptions({
+      editor.setTheme($.totalStorage("hue.ace.theme") || "ace/theme/hue");
+
+      var _editorOptions = {
         enableBasicAutocompletion: true,
         enableSnippets: true,
         enableLiveAutocompletion: true,
-        showGutter: false
-      });
+        showGutter: true,
+        showLineNumbers: false
+      }
+
+      var _extraOptions = $.totalStorage("hue.ace.options") || {};
+      $.extend( _editorOptions, _options.editorOptions || _extraOptions);
+
+      editor.setOptions(_editorOptions);
       editor.on("blur", function () {
         _options.value(editor.getValue());
         _onBlur(editor);
       });
-      editor.on("change", function () {
-        _onChange(editor);
+      editor.on("change", function (e) {
+        editor.clearErrors();
+        _onChange(e, editor, valueAccessor);
+      });
+
+      editor.commands.on("afterExec", function(e) {
+
+        // if it's pig and before it's LOAD ' we disable the autocomplete and show a filechooser btn
+        if (editor.session.getMode().$id = "ace/mode/pig" && e.args) {
+          var _textBefore = editor.getTextBeforeCursor();
+          if ((e.args == "'" && _textBefore.toUpperCase().indexOf("LOAD ") > -1 && _textBefore.toUpperCase().indexOf("LOAD ") == _textBefore.toUpperCase().length - 5)
+              || _textBefore.toUpperCase().indexOf("LOAD '") > -1 && _textBefore.toUpperCase().indexOf("LOAD '") == _textBefore.toUpperCase().length - 6){
+            editor.disableAutocomplete();
+            var _btn = editor.showFileButton();
+            _btn.on("click", function(e){
+              e.preventDefault();
+              if ($(".filechooser-content").data("spinner") == null){
+                $(".filechooser-content").data("spinner", $(".filechooser-content").html());
+              }
+              else {
+                $(".filechooser-content").html($(".filechooser-content").data("spinner"));
+              }
+              $(".filechooser-content").jHueFileChooser({
+                onFileChoose: function (filePath) {
+                  editor.session.insert(editor.getCursorPosition(), filePath);
+                  editor.hideFileButton();
+                  editor.enableAutocomplete();
+                  $(".filechooser").hide();
+                },
+                selectFolder: false,
+                createFolder: false
+              });
+              $(".filechooser").css({ "top": $(e.currentTarget).position().top, "left": $(e.currentTarget).position().left}).show();
+            });
+          }
+          else {
+            editor.hideFileButton();
+            editor.enableAutocomplete();
+          }
+          if (e.args != "'" && _textBefore.toUpperCase().indexOf("LOAD '") > -1 && _textBefore.toUpperCase().indexOf("LOAD '") == _textBefore.toUpperCase().length - 6) {
+            editor.hideFileButton();
+            editor.enableAutocomplete();
+          }
+        }
+
+        _onAfterExec(e, editor, valueAccessor);
       });
+
       editor.$blockScrolling = Infinity
-      element.editor = editor;
-      element.editor.originalCompleters = editor.completers;
+      element.originalCompleters = editor.completers;
+      _options.ace(editor);
     },
     update: function (element, valueAccessor) {
       var _options = ko.unwrap(valueAccessor());
-      if (element.editor) {
-        element.editor.completers = element.editor.originalCompleters.slice();
-        _options.extraCompleters().forEach(function (complete) {
-          element.editor.completers.push(complete);
-        });
+      if (_options.ace()) {
+        var _editor = _options.ace();
+        _editor.completers = element.originalCompleters.slice();
+        if (_options.extraCompleters().length > 0) {
+          _options.extraCompleters().forEach(function (complete) {
+            _editor.completers.push(complete);
+          });
+          _editor.execCommand("startAutocomplete");
+        }
+        _editor.clearErrors();
+        if (_options.errors().length > 0){
+          _options.errors().forEach(function(err){
+            _editor.addError(err.message, err.line);
+            if (err.line == _editor.getCursorPosition().row){
+              window.setTimeout(function(){
+                $(element).find(".ace_active-line").remove();
+              }, 100)
+            }
+          });
+        }
       }
     }
   }