You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by "ASF GitHub Bot (JIRA)" <ji...@apache.org> on 2018/11/01 21:09:01 UTC

[jira] [Commented] (AIRFLOW-2278) Add possibility to edit DAGs in webapp

    [ https://issues.apache.org/jira/browse/AIRFLOW-2278?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=16672205#comment-16672205 ] 

ASF GitHub Bot commented on AIRFLOW-2278:
-----------------------------------------

ashb closed pull request #3185: [AIRFLOW-2278] Add possibility to edit DAGs in webapp
URL: https://github.com/apache/incubator-airflow/pull/3185
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/airflow/www/static/main.css b/airflow/www/static/main.css
index 57164b94e5..7d44e1a528 100644
--- a/airflow/www/static/main.css
+++ b/airflow/www/static/main.css
@@ -146,6 +146,24 @@ input, select {
 #sql_ace {
     visibility: hidden;
 }
+#dag_editor {
+    margin: 10px 0px;
+    width: 100%;
+    height: 100px;
+}
+#dag_edited {
+    visibility: hidden;
+    display: none;
+}
+.dag-header {
+    display: flex;
+}
+.dag-header h4{
+    flex-grow: 1;
+}
+.dag-header .btn{
+    float: right;
+}
 table.dataframe {
     font-size: 12px;
 }
diff --git a/airflow/www/static/mode-python.js b/airflow/www/static/mode-python.js
new file mode 100644
index 0000000000..8c270bd867
--- /dev/null
+++ b/airflow/www/static/mode-python.js
@@ -0,0 +1,291 @@
+/**
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements. See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership. The ASF 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.
+*/
+
+define("ace/mode/python_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"], function(require, exports, module) {
+"use strict";
+
+var oop = require("../lib/oop");
+var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules;
+
+var PythonHighlightRules = function() {
+
+    var keywords = (
+        "and|as|assert|break|class|continue|def|del|elif|else|except|exec|" +
+        "finally|for|from|global|if|import|in|is|lambda|not|or|pass|print|" +
+        "raise|return|try|while|with|yield|async|await"
+    );
+
+    var builtinConstants = (
+        "True|False|None|NotImplemented|Ellipsis|__debug__"
+    );
+
+    var builtinFunctions = (
+        "abs|divmod|input|open|staticmethod|all|enumerate|int|ord|str|any|" +
+        "eval|isinstance|pow|sum|basestring|execfile|issubclass|print|super|" +
+        "binfile|iter|property|tuple|bool|filter|len|range|type|bytearray|" +
+        "float|list|raw_input|unichr|callable|format|locals|reduce|unicode|" +
+        "chr|frozenset|long|reload|vars|classmethod|getattr|map|repr|xrange|" +
+        "cmp|globals|max|reversed|zip|compile|hasattr|memoryview|round|" +
+        "__import__|complex|hash|min|set|apply|delattr|help|next|setattr|" +
+        "buffer|dict|hex|object|slice|coerce|dir|id|oct|sorted|intern"
+    );
+
+    //var futureReserved = "";
+    var keywordMapper = this.createKeywordMapper({
+        "invalid.deprecated": "debugger",
+        "support.function": builtinFunctions,
+        "variable.language": "self|cls",
+        "constant.language": builtinConstants,
+        "keyword": keywords
+    }, "identifier");
+
+    var strPre = "(?:r|u|ur|R|U|UR|Ur|uR)?";
+
+    var decimalInteger = "(?:(?:[1-9]\\d*)|(?:0))";
+    var octInteger = "(?:0[oO]?[0-7]+)";
+    var hexInteger = "(?:0[xX][\\dA-Fa-f]+)";
+    var binInteger = "(?:0[bB][01]+)";
+    var integer = "(?:" + decimalInteger + "|" + octInteger + "|" + hexInteger + "|" + binInteger + ")";
+
+    var exponent = "(?:[eE][+-]?\\d+)";
+    var fraction = "(?:\\.\\d+)";
+    var intPart = "(?:\\d+)";
+    var pointFloat = "(?:(?:" + intPart + "?" + fraction + ")|(?:" + intPart + "\\.))";
+    var exponentFloat = "(?:(?:" + pointFloat + "|" +  intPart + ")" + exponent + ")";
+    var floatNumber = "(?:" + exponentFloat + "|" + pointFloat + ")";
+
+    var stringEscape =  "\\\\(x[0-9A-Fa-f]{2}|[0-7]{3}|[\\\\abfnrtv'\"]|U[0-9A-Fa-f]{8}|u[0-9A-Fa-f]{4})";
+
+    this.$rules = {
+        "start" : [ {
+            token : "comment",
+            regex : "#.*$"
+        }, {
+            token : "string",           // multi line """ string start
+            regex : strPre + '"{3}',
+            next : "qqstring3"
+        }, {
+            token : "string",           // " string
+            regex : strPre + '"(?=.)',
+            next : "qqstring"
+        }, {
+            token : "string",           // multi line ''' string start
+            regex : strPre + "'{3}",
+            next : "qstring3"
+        }, {
+            token : "string",           // ' string
+            regex : strPre + "'(?=.)",
+            next : "qstring"
+        }, {
+            token : "constant.numeric", // imaginary
+            regex : "(?:" + floatNumber + "|\\d+)[jJ]\\b"
+        }, {
+            token : "constant.numeric", // float
+            regex : floatNumber
+        }, {
+            token : "constant.numeric", // long integer
+            regex : integer + "[lL]\\b"
+        }, {
+            token : "constant.numeric", // integer
+            regex : integer + "\\b"
+        }, {
+            token : keywordMapper,
+            regex : "[a-zA-Z_$][a-zA-Z0-9_$]*\\b"
+        }, {
+            token : "keyword.operator",
+            regex : "\\+|\\-|\\*|\\*\\*|\\/|\\/\\/|%|<<|>>|&|\\||\\^|~|<|>|<=|=>|==|!=|<>|="
+        }, {
+            token : "paren.lparen",
+            regex : "[\\[\\(\\{]"
+        }, {
+            token : "paren.rparen",
+            regex : "[\\]\\)\\}]"
+        }, {
+            token : "text",
+            regex : "\\s+"
+        } ],
+        "qqstring3" : [ {
+            token : "constant.language.escape",
+            regex : stringEscape
+        }, {
+            token : "string", // multi line """ string end
+            regex : '"{3}',
+            next : "start"
+        }, {
+            defaultToken : "string"
+        } ],
+        "qstring3" : [ {
+            token : "constant.language.escape",
+            regex : stringEscape
+        }, {
+            token : "string",  // multi line ''' string end
+            regex : "'{3}",
+            next : "start"
+        }, {
+            defaultToken : "string"
+        } ],
+        "qqstring" : [{
+            token : "constant.language.escape",
+            regex : stringEscape
+        }, {
+            token : "string",
+            regex : "\\\\$",
+            next  : "qqstring"
+        }, {
+            token : "string",
+            regex : '"|$',
+            next  : "start"
+        }, {
+            defaultToken: "string"
+        }],
+        "qstring" : [{
+            token : "constant.language.escape",
+            regex : stringEscape
+        }, {
+            token : "string",
+            regex : "\\\\$",
+            next  : "qstring"
+        }, {
+            token : "string",
+            regex : "'|$",
+            next  : "start"
+        }, {
+            defaultToken: "string"
+        }]
+    };
+};
+
+oop.inherits(PythonHighlightRules, TextHighlightRules);
+
+exports.PythonHighlightRules = PythonHighlightRules;
+});
+
+define("ace/mode/folding/pythonic",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"],function(require, exports, module) {
+"use strict";
+
+var oop = require("../../lib/oop");
+var BaseFoldMode = require("./fold_mode").FoldMode;
+
+var FoldMode = exports.FoldMode = function(markers) {
+    this.foldingStartMarker = new RegExp("([\\[{])(?:\\s*)$|(" + markers + ")(?:\\s*)(?:#.*)?$");
+};
+oop.inherits(FoldMode, BaseFoldMode);
+
+(function() {
+
+    this.getFoldWidgetRange = function(session, foldStyle, row) {
+        var line = session.getLine(row);
+        var match = line.match(this.foldingStartMarker);
+        if (match) {
+            if (match[1])
+                return this.openingBracketBlock(session, match[1], row, match.index);
+            if (match[2])
+                return this.indentationBlock(session, row, match.index + match[2].length);
+            return this.indentationBlock(session, row);
+        }
+    };
+
+}).call(FoldMode.prototype);
+
+});
+
+define("ace/mode/python",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/python_highlight_rules","ace/range", "ace/mode/folding/pythonic"], function(require, exports, module) {
+"use strict";
+
+var oop = require("../lib/oop");
+var TextMode = require("./text").Mode;
+var PythonHighlightRules = require("./python_highlight_rules").PythonHighlightRules;
+var PythonFoldMode = require("./folding/pythonic").FoldMode;
+var Range = require("../range").Range;
+
+var Mode = function() {
+    this.HighlightRules = PythonHighlightRules;
+    this.foldingRules = new PythonFoldMode("\\:");
+    this.$behaviour = this.$defaultBehaviour;
+};
+oop.inherits(Mode, TextMode);
+
+(function() {
+
+    this.lineCommentStart = "#";
+
+    this.getNextLineIndent = function(state, line, tab) {
+        var indent = this.$getIndent(line);
+
+        var tokenizedLine = this.getTokenizer().getLineTokens(line, state);
+        var tokens = tokenizedLine.tokens;
+
+        if (tokens.length && tokens[tokens.length-1].type == "comment") {
+            return indent;
+        }
+
+        if (state == "start") {
+            var match = line.match(/^.*[\{\(\[:]\s*$/);
+            if (match) {
+                indent += tab;
+            }
+        }
+
+        return indent;
+    };
+
+    var outdents = {
+        "pass": 1,
+        "return": 1,
+        "raise": 1,
+        "break": 1,
+        "continue": 1
+    };
+    
+    this.checkOutdent = function(state, line, input) {
+        if (input !== "\r\n" && input !== "\r" && input !== "\n")
+            return false;
+
+        var tokens = this.getTokenizer().getLineTokens(line.trim(), state).tokens;
+        
+        if (!tokens)
+            return false;
+        
+        // ignore trailing comments
+        do {
+            var last = tokens.pop();
+        } while (last && (last.type == "comment" || (last.type == "text" && last.value.match(/^\s+$/))));
+        
+        if (!last)
+            return false;
+        
+        return (last.type == "keyword" && outdents[last.value]);
+    };
+
+    this.autoOutdent = function(state, doc, row) {
+        // outdenting in python is slightly different because it always applies
+        // to the next line and only of a new line is inserted
+        
+        row += 1;
+        var indent = this.$getIndent(doc.getLine(row));
+        var tab = doc.getTabString();
+        if (indent.slice(-tab.length) == tab)
+            doc.remove(new Range(row, indent.length-tab.length, row, indent.length));
+    };
+
+    this.$id = "ace/mode/python";
+}).call(Mode.prototype);
+
+exports.Mode = Mode;
+});
diff --git a/airflow/www/templates/airflow/dag_code.html b/airflow/www/templates/airflow/dag_code.html
index 35f85b2f78..d8400b26e1 100644
--- a/airflow/www/templates/airflow/dag_code.html
+++ b/airflow/www/templates/airflow/dag_code.html
@@ -20,39 +20,70 @@
 
 {% block body %}
     {{ super() }}
-    <h4>{{ title }}</h4>
-    {% if html_code %}
-        {{ html_code|safe }}
-    {% endif %}
-    {% if code %}
-        <pre>{{ code }}</pre>
-    {% endif %}
-
-    {% if code_dict %}
-        {% for k, v in code_dict.items() %}
-            <h5>{{ k }}</h5>
-            <pre>{{ v }}</pre>
-        {% endfor %}
-    {% endif %}
-    {% if html_dict %}
-        {% for k, v in html_dict.items() %}
-            <h5>{{ k }}</h5>
-            {{ v|safe }}
-        {% endfor %}
+    {% if (is_editable and code) %}
+      <form method="post" id="dag_form">
+        <div class="form-inline">
+          <div class="dag-header">
+            <h4>{{ title }}</h4>
+            <input name="_csrf_token" type="hidden" value="{{ csrf_token() }}">
+            <input type="submit" class="btn btn-primary" value="Save">
+          </div>
+          <span id="results"></span><br>
+          <div id='dag_editor' style="visibility: hidden;">{{ code|safe }}</div>
+          <textarea id="dag_edited" name="dag_edited">{{ code|safe }}</textarea>
+        </div>
+      </form>
+    {% else %}
+      <h4>{{ title }}</h4>
+      {% if html_code %}
+          {{ html_code|safe }}
+      {% endif %}
+      {% if code_dict %}
+          {% for k, v in code_dict.items() %}
+              <h5>{{ k }}</h5>
+              <pre>{{ v }}</pre>
+          {% endfor %}
+      {% endif %}
+      {% if html_dict %}
+          {% for k, v in html_dict.items() %}
+              <h5>{{ k }}</h5>
+              {{ v|safe }}
+          {% endfor %}
+      {% endif %}
     {% endif %}
 {% endblock %}
 
 {% block tail %}
-    {{ super() }}
-    <script>
-      // We blur task_ids in demo mode
+  {{ super() }}
+  <script src="{{ url_for('static', filename='ace.js') }}"></script>
+  <script src="{{ url_for('static', filename='mode-python.js') }}"></script>
+  <script>
     $( document ).ready(function() {
+      {% if (is_editable and code) %}
+        var editor = ace.edit("dag_editor");
+        var textarea = $('textarea[name="dag_edited"]').hide();
+        function sync() {
+            textarea.val(editor.getSession().getValue());
+        }
+        editor.setTheme("ace/theme/crimson_editor");
+        editor.setOptions({
+            minLines: 3,
+            maxLines: Infinity,
+        });
+        editor.getSession().setMode("ace/mode/python");
+        editor.getSession().on('change', sync);
+        // Prevents ugly code look before ace editor has rendered it
+        $('#dag_editor').css('visibility', 'visible');
+        editor.focus();
+      {% endif %}
+
+      // We blur task_ids in demo mode
       if ("{{ demo_mode }}" == "True") {
-          $("pre span.s").css({
-              'text-shadow': '0px 0px 10px red',
-              'color': 'transparent',
-          });
+        $("pre span.s").css({
+            'text-shadow': '0px 0px 10px red',
+            'color': 'transparent',
+        });
       }
     });
-    </script>
+  </script>
 {% endblock %}
diff --git a/airflow/www/views.py b/airflow/www/views.py
index 83e567a66d..8ee7df9162 100644
--- a/airflow/www/views.py
+++ b/airflow/www/views.py
@@ -607,12 +607,38 @@ def task_stats(self, session=None):
                 payload[dag.safe_dag_id].append(d)
         return wwwutils.json_response(payload)
 
-    @expose('/code')
+    @expose('/code', methods=["GET", "POST"])
     @login_required
     def code(self):
+        # Checking access for given DAG for current user
+        def is_dag_editable(dag):
+            if 'editable_by' in dag.default_args:
+                if (
+                    dag.default_args['editable_by'] == '*' or (
+                        not current_user.is_anonymous and
+                        current_user.user.username in dag.default_args['editable_by']
+                    )
+                ):
+                    return True
+                else:
+                    return False
+            else:
+                return False
+
         dag_id = request.args.get('dag_id')
         dag = dagbag.get_dag(dag_id)
         title = dag_id
+
+        # Writing changes to DAG file (if were made) and refreshing DAG
+        if is_dag_editable(dag) and request.form.get('dag_edited'):
+            try:
+                with open(dag.fileloc, 'w') as f:
+                    f.write(request.form.get('dag_edited'))
+                self.refresh("DAG [{}] has been saved and refreshed")
+                dag = dagbag.get_dag(dag_id)
+            except IOError as e:
+                flash('Error writing DAG to file: ' + str(e), 'error')
+
         try:
             with open(dag.fileloc, 'r') as f:
                 code = f.read()
@@ -622,7 +648,12 @@ def code(self):
             html_code = str(e)
 
         return self.render(
-            'airflow/dag_code.html', html_code=html_code, dag=dag, title=title,
+            'airflow/dag_code.html',
+            html_code=html_code,
+            code=code,
+            dag=dag,
+            title=title,
+            is_editable=is_dag_editable(dag),
             root=request.args.get('root'),
             demo_mode=conf.getboolean('webserver', 'demo_mode'))
 
@@ -1669,7 +1700,7 @@ def paused(self, session=None):
     @login_required
     @wwwutils.action_logging
     @provide_session
-    def refresh(self, session=None):
+    def refresh(self, session=None, messageText=None):
         DagModel = models.DagModel
         dag_id = request.args.get('dag_id')
         orm_dag = session.query(
@@ -1681,7 +1712,8 @@ def refresh(self, session=None):
         session.commit()
 
         dagbag.get_dag(dag_id)
-        flash("DAG [{}] is now fresh as a daisy".format(dag_id))
+        flashText = messageText if messageText else "DAG [{}] is now fresh as a daisy"
+        flash(flashText.format(dag_id))
         return redirect(request.referrer)
 
     @expose('/refresh_all')


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


> Add possibility to edit DAGs in webapp
> --------------------------------------
>
>                 Key: AIRFLOW-2278
>                 URL: https://issues.apache.org/jira/browse/AIRFLOW-2278
>             Project: Apache Airflow
>          Issue Type: Improvement
>          Components: webapp
>            Reporter: Mykola Mykhalov
>            Assignee: Mykola Mykhalov
>            Priority: Minor
>
> When you need to make some minor changes in your DAG would be nice to have possibility to edit it from webapp.
> To protect DAG from editing by everyone should be set up arg inside DAG 'editable_by'. Where '*' means everyone, and  ['user1', 'user2'] means specific users.
> Example when DAG can be editable in webapp by user1 and user2 only:
> args = {
>  'owner': 'airflow',
>  'editable_by': ['user1', 'user2'],
>  'start_date': airflow.utils.dates.days_ago(2)
> }



--
This message was sent by Atlassian JIRA
(v7.6.3#76005)