You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by bo...@apache.org on 2018/03/23 08:19:17 UTC

[03/14] incubator-airflow git commit: [AIRFLOW-1433][AIRFLOW-85] New Airflow Webserver UI with RBAC support

http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/05e1861e/airflow/www_rbac/templates/airflow/graph.html
----------------------------------------------------------------------
diff --git a/airflow/www_rbac/templates/airflow/graph.html b/airflow/www_rbac/templates/airflow/graph.html
new file mode 100644
index 0000000..061cad6
--- /dev/null
+++ b/airflow/www_rbac/templates/airflow/graph.html
@@ -0,0 +1,369 @@
+{# 
+  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.
+
+#}
+{% extends "airflow/dag.html" %}
+
+{% block title %}Airflow - DAGs{% endblock %}
+
+{% block head_css %}
+{{ super() }}
+<link rel="stylesheet" type="text/css"
+    href="{{ url_for('static', filename='dagre.css') }}">
+<link rel="stylesheet" type="text/css"
+    href="{{ url_for('static', filename='graph.css') }}">
+{% endblock %}
+
+{% block content %}
+{{ super() }}
+    {% if dag.doc_md %}
+        <div class="rich_doc" style="margin-bottom: 15px;">{{ doc_md|safe }}</div>
+    {% endif %}
+    <div class="form-inline">
+        <form method="get" style="float:left;">
+            {{ state_token }}
+            Run:
+            {{ form.execution_date(class_="form-control") | safe }}
+            Layout:
+            {{ form.arrange(class_="form-control") | safe }}
+            <input type="hidden" name="root" value="{{ root }}">
+            <input type="hidden" value="{{ dag.dag_id }}" name="dag_id">
+            <input name="_csrf_token" type="hidden" value="{{ csrf_token() }}">
+            <input type="submit" value="Go" class="btn btn-default"
+            action="" method="get">
+        </form>
+            <div class="input-group" style="float: right;">
+              <input type="text" id="searchbox" class="form-control" placeholder="Search for..." onenter="null">
+            </div><!-- /input-group -->
+        <div style="clear: both;">
+    </div>
+<hr/>
+<div>
+    {% for op in operators %}
+    <div class="legend_item" style="border-width:1px;float:left;background:{{ op.ui_color }};color:{{ op.ui_fgcolor }};">
+        {{ op.__name__ }}
+    </div>
+    {% endfor %}
+
+    <div style"background-color: blue;">
+    <div class="legend_item state" style="border-color:white;">no status</div>
+    <div class="legend_item state" style="border-color:grey;">queued</div>
+    <div class="legend_item state" style="border-color:gold;">retry</div>
+    <div class="legend_item state" style="border-color:pink;">skipped</div>
+    <div class="legend_item state" style="border-color:red;">failed</div>
+    <div class="legend_item state" style="border-color:lime;">running</div>
+    <div class="legend_item state" style="border-color:green;">success</div>
+    </div>
+    <div style="clear:both;"></div>
+</div>
+<div id="error" style="display: none; margin-top: 10px;" class="alert alert-danger" role="alert">
+        <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
+        <span id="error_msg">Oops.</span>
+</div>
+<hr style="margin-bottom: 0px;"/>
+<button class="btn btn-default pull-right" id="refresh_button">
+    <span class="glyphicon glyphicon-refresh" aria-hidden="true"></span>
+</button>
+<div id="svg_container">
+
+    <svg width="{{ width }}" height="{{ height }}">
+        <g id='dig' transform="translate(20,20)"/>
+        <filter id="blur-effect-1">
+        <feGaussianBlur stdDeviation="3" />
+        </filter>
+    </svg>
+    <img id="loading" alt="spinner" src="{{ url_for('static', filename='loading.gif') }}">
+</div>
+<hr>
+{% endblock %}
+
+{% block tail %}
+    {{ super() }}
+
+    <script src="{{ url_for('static', filename='d3.v3.min.js') }}"></script>
+    <script src="{{ url_for('static', filename='dagre-d3.js') }}"></script>
+    <script>
+
+    var highlight_color = "#000000";
+    var upstream_color = "#2020A0";
+    var downstream_color = "#0000FF";
+
+    var nodes = {{ nodes|safe }};
+    var edges = {{ edges|safe }};
+    var tasks = {{ tasks|safe }};
+    var task_instances = {{ task_instances|safe }};
+    var execution_date = "{{ execution_date }}";
+    var arrange = "{{ arrange }}";
+    var g = dagreD3.json.decode(nodes, edges);
+    var duration = 500;
+    var stateFocusMap = {
+        'no status':false, 'failed':false, 'running':false,
+        'queued': false, 'success': false};
+
+
+    var layout = dagreD3.layout().rankDir(arrange).nodeSep(15).rankSep(15);
+    var renderer = new dagreD3.Renderer();
+    renderer.layout(layout).run(g, d3.select("#dig"));
+    inject_node_ids(tasks);
+    update_nodes_states(task_instances);
+
+    d3.selectAll("g.node").on("click", function(d){
+        task = tasks[d];
+        if (task.task_type == "SubDagOperator")
+            call_modal(d, execution_date, true);
+        else
+            call_modal(d, execution_date);
+    });
+
+
+    function highlight_nodes(nodes, color) {
+        nodes.forEach (function (nodeid) {
+            my_node = d3.select('#' + nodeid + ' rect');
+            my_node.style("stroke", color) ;
+        })
+    }
+
+    d3.selectAll("g.node").on("mouseover", function(d){
+        d3.select(this).selectAll("rect").style("stroke", highlight_color) ;
+        highlight_nodes(g.predecessors(d), upstream_color)
+        highlight_nodes(g.successors(d), downstream_color)
+
+    });
+
+    d3.selectAll("g.node").on("mouseout", function(d){
+        d3.select(this).selectAll("rect").style("stroke", null) ;
+        highlight_nodes(g.predecessors(d), null)
+        highlight_nodes(g.successors(d), null)
+    });
+
+
+    {% if blur %}
+    d3.selectAll("text").attr("class", "blur");
+    {% endif %}
+
+    $("g.node").tooltip({
+      html: true,
+      container: "body",
+    });
+
+    d3.selectAll("div.legend_item.state")
+        .style("cursor", "pointer")
+        .on("mouseover", function(){
+            if(!stateIsSet()){
+                state = d3.select(this).text();
+                focusState(state);
+            }
+        })
+        .on("mouseout", function(){
+            if(!stateIsSet()){
+                clearFocus();
+            }
+        });
+
+    d3.selectAll("div.legend_item.state")
+        .on("click", function(){
+            state = d3.select(this).text();
+            color = d3.select(this).style("border-color");
+
+            if (!stateFocusMap[state]){
+                clearFocus();
+                focusState(state, this, color);
+                setFocusMap(state);
+
+            } else {
+                clearFocus();
+                setFocusMap();
+            }
+        });
+
+    d3.select("#searchbox").on("keyup", function(){
+        var s = document.getElementById("searchbox").value;
+        var match = null;
+
+        if (stateIsSet){
+            clearFocus();
+            setFocusMap();
+        }
+
+        d3.selectAll("g.nodes g.node").filter(function(d, i){
+            if (s==""){
+                d3.select("g.edgePaths")
+                    .transition().duration(duration)
+                    .style("opacity", 1);
+                d3.select(this)
+                    .transition().duration(duration)
+                    .style("opacity", 1)
+                    .selectAll("rect")
+                    .style("stroke-width", "2px");
+            }
+            else{
+                d3.select("g.edgePaths")
+                    .transition().duration(duration)
+                    .style("opacity", 0.2);
+                if (d.indexOf(s) > -1) {
+                    if (!match)
+                        match = this;
+                    d3.select(this)
+                        .transition().duration(duration)
+                        .style("opacity", 1)
+                        .selectAll("rect")
+                        .style("stroke-width", "10px");
+                }
+                else {
+                    d3.select(this)
+                        .transition()
+                        .style("opacity", 0.2).duration(duration)
+                        .selectAll("rect")
+                        .style("stroke-width", "2px");
+                }
+            }
+        });
+        if(match) {
+            var transform = d3.transform(d3.select(match).attr("transform"));
+            transform.translate = [
+                -transform.translate[0] + 520,
+                -(transform.translate[1] - 400)
+            ];
+            transform.scale = [1, 1];
+
+            d3.select("g.zoom")
+                .transition()
+                .attr("transform", transform.toString());
+            renderer.zoom_obj.translate(transform.translate);
+            renderer.zoom_obj.scale(1);
+        }
+    });
+
+
+    // Injecting ids to be used for parent/child highlighting
+    // Separated from update_node_states since it must work even
+    // when there is no valid task instance available
+    function inject_node_ids(tasks) {
+        $.each(tasks, function(task_id, task) {
+            $('tspan').filter(function(index) { return $(this).text() === task_id; })
+                    .parent().parent().parent()
+                    .attr("id", task_id);
+        });
+    }
+
+
+    // Assigning css classes based on state to nodes
+    function update_nodes_states(task_instances) {
+        $.each(task_instances, function(task_id, ti) {
+          $('tspan').filter(function(index) { return $(this).text() === task_id; })
+            .parent().parent().parent()
+            .attr("class", "node enter " + ti.state)
+            .attr("data-toggle", "tooltip")
+            .attr("data-original-title", function(d) {
+              // Tooltip
+              task = tasks[task_id];
+              tt =  "Task_id: " + ti.task_id + "<br>";
+              tt += "Run: " + ti.execution_date + "<br>";
+              if(ti.run_id != undefined){
+                tt += "run_id: <nobr>" + ti.run_id + "</nobr><br>";
+              }
+              tt += "Operator: " + task.task_type + "<br>";
+              tt += "Started: " + ti.start_date + "<br>";
+              tt += "Ended: " + ti.end_date + "<br>";
+              tt += "Duration: " + ti.duration + "<br>";
+              tt += "State: " + ti.state + "<br>";
+              return tt;
+            });
+        });
+    }
+
+    function clearFocus(){
+        d3.selectAll("g.node")
+            .transition(duration)
+            .style("opacity", 1);
+        d3.selectAll("g.node rect")
+            .transition(duration)
+            .style("stroke-width", "2px");
+        d3.select("g.edgePaths")
+            .transition().duration(duration)
+            .style("opacity", 1);
+        d3.selectAll("div.legend_item.state")
+            .style("background-color", null);
+    }
+
+    function focusState(state, node, color){
+        d3.selectAll("g.node")
+            .transition(duration)
+            .style("opacity", 0.2);
+        d3.selectAll("g.node."+state)
+            .transition(duration)
+            .style("opacity", 1);
+        d3.selectAll("g.node." + state + " rect")
+            .transition(duration)
+            .style("stroke-width", "10px")
+            .style("opacity", 1);
+        d3.select("g.edgePaths")
+            .transition().duration(duration)
+            .style("opacity", 0.2);
+        d3.select(node)
+            .style("background-color", color);
+    }
+
+    function setFocusMap(state){
+        for (var key in stateFocusMap){
+                stateFocusMap[key] = false;
+        }
+        if(state != null){
+            stateFocusMap[state] = true;
+        }
+    }
+
+    function stateIsSet(){
+        for (var key in stateFocusMap){
+            if (stateFocusMap[key]){
+                return true
+            }
+        }
+        return false
+    }
+
+    function error(msg){
+      $('#error_msg').html(msg);
+      $('#error').show();
+      $('#loading').hide();
+      $('#chart_section').hide(1000);
+      $('#datatable_section').hide(1000);
+    }
+
+    d3.select("#refresh_button").on("click",
+        function() {
+            $("#loading").css("display", "block");
+            $("div#svg_container").css("opacity", "0.2");
+            $.get(
+                "/airflow/object/task_instances",
+                {dag_id : "{{ dag.dag_id }}", execution_date : "{{ execution_date }}"})
+            .done(
+                function(task_instances) {
+                    update_nodes_states(JSON.parse(task_instances));
+                    $("#loading").hide();
+                    $("div#svg_container").css("opacity", "1");
+                    $('#error').hide();
+                }
+            ).fail(function(jqxhr, textStatus, err) {
+                error(textStatus + ': ' + err);
+            });
+        }
+    );
+
+    </script>
+
+
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/05e1861e/airflow/www_rbac/templates/airflow/master.html
----------------------------------------------------------------------
diff --git a/airflow/www_rbac/templates/airflow/master.html b/airflow/www_rbac/templates/airflow/master.html
new file mode 100644
index 0000000..92a4b9a
--- /dev/null
+++ b/airflow/www_rbac/templates/airflow/master.html
@@ -0,0 +1,18 @@
+{# 
+  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.
+
+#}
+{% extends "appbuilder/baselayout.html" %}

http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/05e1861e/airflow/www_rbac/templates/airflow/model_list.html
----------------------------------------------------------------------
diff --git a/airflow/www_rbac/templates/airflow/model_list.html b/airflow/www_rbac/templates/airflow/model_list.html
new file mode 100644
index 0000000..048109f
--- /dev/null
+++ b/airflow/www_rbac/templates/airflow/model_list.html
@@ -0,0 +1,93 @@
+{# 
+  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.
+
+#}
+
+{% import 'appbuilder/general/lib.html' as lib %}
+{% extends 'appbuilder/general/widgets/base_list.html' %}
+
+
+    {% block begin_content scoped %}
+        <div class="table-responsive">
+        <table class="table table-bordered table-hover">
+    {% endblock %}
+
+    {% block begin_loop_header scoped %}
+        <thead>
+        <tr>
+        {% if actions %}
+        <th class="action_checkboxes">
+            <input id="check_all" class="action_check_all" name="check_all" type="checkbox">
+        </th>
+        {% endif %}
+
+        {% if can_show or can_edit or can_delete %}
+            <th class="col-md-1 col-lg-1 col-sm-1" ></th>
+        {% endif %}
+
+        {% for item in include_columns %}
+            {% if item in order_columns %}
+                {% set res = item | get_link_order(modelview_name) %}
+                    {% if res == 2 %}
+                    <th><a href={{ item | link_order(modelview_name) }}>{{label_columns.get(item)}}
+                    <i class="fa fa-chevron-up pull-right"></i></a></th>
+                {% elif res == 1 %}
+                    <th><a href={{ item | link_order(modelview_name) }}>{{label_columns.get(item)}}
+                    <i class="fa fa-chevron-down pull-right"></i></a></th>
+                {% else %}
+                    <th><a href={{ item | link_order(modelview_name) }}>{{label_columns.get(item)}}
+                    <i class="fa fa-arrows-v pull-right"></i></a></th>
+                {% endif %}
+            {% else %}
+                <th>{{label_columns.get(item)}}</th>
+            {% endif %}
+        {% endfor %}
+        </tr>
+        </thead>
+    {% endblock %}
+
+    {% block begin_loop_values %}
+        {% for item in value_columns %}
+            {% set pk = pks[loop.index-1] %}
+            <tr>
+                {% if actions %}
+                <td>
+                    <input id="{{pk}}" class="action_check" name="rowid" value="{{pk}}" type="checkbox">
+                </td>
+                {% endif %}
+                {% if can_show or can_edit or can_delete %}
+                    <td><center>
+                    {{ lib.btn_crud(can_show, can_edit, can_delete, pk, modelview_name, filters) }}
+                    </center></td>
+                {% endif %}
+                {% for value in include_columns %}
+                    {% set formatter = formatters_columns.get(value) %}
+                    {% if formatter and formatter(item) %}
+                        <td>{{ formatter(item) }}</td>
+                    {% elif item[value] != None %}
+                        <td>{{ item[value]|safe }}</td>
+                    {% else %}
+                        <td></td>
+                    {% endif %}
+                {% endfor %}
+            </tr>
+        {% endfor %}
+    {% endblock %}
+
+    {% block end_content scoped %}
+        </table>
+        </div>
+    {% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/05e1861e/airflow/www_rbac/templates/airflow/noaccess.html
----------------------------------------------------------------------
diff --git a/airflow/www_rbac/templates/airflow/noaccess.html b/airflow/www_rbac/templates/airflow/noaccess.html
new file mode 100644
index 0000000..317d50a
--- /dev/null
+++ b/airflow/www_rbac/templates/airflow/noaccess.html
@@ -0,0 +1,24 @@
+{# 
+  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.
+
+#}
+{% extends "airflow/master.html" %}
+
+{% block title %}{{ title }}{% endblock %}
+
+{% block content %}
+You don't seem to have access. Please contact your administrator.
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/05e1861e/airflow/www_rbac/templates/airflow/task.html
----------------------------------------------------------------------
diff --git a/airflow/www_rbac/templates/airflow/task.html b/airflow/www_rbac/templates/airflow/task.html
new file mode 100644
index 0000000..6c843b2
--- /dev/null
+++ b/airflow/www_rbac/templates/airflow/task.html
@@ -0,0 +1,75 @@
+{# 
+  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.
+
+#}
+{% extends "airflow/task_instance.html" %}
+{% block title %}Airflow - DAGs{% endblock %}
+
+{% block content %}
+    {{ super() }}
+    <h4>{{ title }}</h4>
+    <div>
+        <h5>Dependencies Blocking Task From Getting Scheduled</h5>
+        <table class="table table-striped table-bordered">
+            <tr>
+                <th>Dependency</th>
+                <th>Reason</th>
+            </tr>
+            {% for dependency, reason in failed_dep_reasons %}
+                <tr>
+                    <td>{{ dependency }}</td>
+                    {% autoescape false %}
+                    <td class='code'>{{ reason }}</td>
+                    {% endautoescape %}
+                </tr>
+            {% endfor %}
+        </table>
+        {{ html_code|safe }}
+    </div>
+    <div>
+        {% for attr, value in special_attrs_rendered.items() %}
+            <h5>Attribute: {{ attr }}</h5>
+            {{ value|safe }}
+        {% endfor %}
+        <h5>Task Instance Attributes</h5>
+        <table class="table table-striped table-bordered">
+            <tr>
+                <th>Attribute</th>
+                <th>Value</th>
+            </tr>
+            {% for attr, value in ti_attrs %}
+                <tr>
+                    <td>{{ attr }}</td>
+                    <td class='code'>{{ value }}</td>
+                </tr>
+            {% endfor %}
+        </table>
+        <h5>Task Attributes</h5>
+        <table class="table table-striped table-bordered">
+            <tr>
+                <th>Attribute</th>
+                <th>Value</th>
+            </tr>
+            {% for attr, value in task_attrs %}
+                <tr>
+                    <td>{{ attr }}</td>
+                    <td class='code'>{{ value }}</td>
+                </tr>
+            {% endfor %}
+        </table>
+        {{ html_code|safe }}
+    </div>
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/05e1861e/airflow/www_rbac/templates/airflow/task_instance.html
----------------------------------------------------------------------
diff --git a/airflow/www_rbac/templates/airflow/task_instance.html b/airflow/www_rbac/templates/airflow/task_instance.html
new file mode 100644
index 0000000..85905bd
--- /dev/null
+++ b/airflow/www_rbac/templates/airflow/task_instance.html
@@ -0,0 +1,75 @@
+{#
+#
+# Licensed 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.
+#
+#}
+{% extends "airflow/dag.html" %}
+
+{% block head_css %}
+{{ super() }}
+<link rel="stylesheet" type="text/css"
+    href="{{ url_for('appbuilder.static',filename='datepicker/bootstrap-datepicker.css')}}" >
+{% endblock %}
+
+{% block content %}
+  {{ super() }}
+  <h4>
+<form method="get" id="daform">
+    <input name="_csrf_token" type="hidden" value="{{ csrf_token() }}">
+    <div class="form-inline">
+            <span style='color:#AAA;'>Task Instance: </span>
+            <span>
+              {{ task_id }}
+              <input type="hidden" value="{{ dag.dag_id }}" name="dag_id">
+            </span>
+		    {{ form.execution_date(class_="form-control") | safe }}
+
+    </div>
+</form>
+  </h4>
+  <ul class="nav nav-pills">
+    <li><a href="{{ url_for("Airflow.task", dag_id=dag.dag_id, task_id=task_id, execution_date=execution_date) }}">
+        <span class="glyphicon glyphicon-certificate" aria-hidden="true"></span>
+      Task Instance Details</a></li>
+    <li><a href="{{ url_for("Airflow.rendered", dag_id=dag.dag_id, task_id=task_id, execution_date=execution_date) }}">
+        <span class="glyphicon glyphicon-certificate" aria-hidden="true"></span>
+      Rendered Template</a></li>
+    <li><a href="{{ url_for("Airflow.log", dag_id=dag.dag_id, task_id=task_id, execution_date=execution_date) }}">
+        <span class="glyphicon glyphicon-certificate" aria-hidden="true"></span>
+      Log</a></li>
+    <li><a href="{{ url_for("Airflow.xcom", dag_id=dag.dag_id, task_id=task_id, execution_date=execution_date) }}">
+        <span class="glyphicon glyphicon-certificate" aria-hidden="true"></span>
+      XCom</a></li>
+  </ul>
+  <hr>
+{% endblock %}
+{% block tail %}
+  {{ super() }}
+  <script src="{{ url_for('appbuilder.static',filename='datepicker/bootstrap-datepicker.js')}}"></script>
+  <script>
+    $( document ).ready(function() {
+      function date_change(){
+          execution_date = $("input#execution_date").val().replace(' ', 'T');
+          loc = decodeURIComponent(window.location.href);
+          loc = loc.replace('{{ execution_date }}', execution_date);
+          window.location = loc;
+      }
+      $("input#execution_date").on("change.daterangepicker", function(){
+          date_change();
+      });
+      $("input#execution_date").on("apply.daterangepicker", function(){
+          date_change();
+      });
+    });
+  </script>
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/05e1861e/airflow/www_rbac/templates/airflow/ti_code.html
----------------------------------------------------------------------
diff --git a/airflow/www_rbac/templates/airflow/ti_code.html b/airflow/www_rbac/templates/airflow/ti_code.html
new file mode 100644
index 0000000..d38eb7d
--- /dev/null
+++ b/airflow/www_rbac/templates/airflow/ti_code.html
@@ -0,0 +1,43 @@
+{# 
+  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.
+
+#}
+{% extends "airflow/task_instance.html" %}
+{% block title %}Airflow - DAGs{% endblock %}
+
+{% block content %}
+    {{ 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 %}
+    {% endif %}
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/05e1861e/airflow/www_rbac/templates/airflow/ti_log.html
----------------------------------------------------------------------
diff --git a/airflow/www_rbac/templates/airflow/ti_log.html b/airflow/www_rbac/templates/airflow/ti_log.html
new file mode 100644
index 0000000..79aee89
--- /dev/null
+++ b/airflow/www_rbac/templates/airflow/ti_log.html
@@ -0,0 +1,40 @@
+{#
+  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.
+
+#}
+{% extends "airflow/task_instance.html" %}
+{% block title %}Airflow - DAGs{% endblock %}
+
+{% block content %}
+  {{ super() }}
+  <h4>{{ title }}</h4>
+  <ul class="nav nav-pills" role="tablist">
+    {% for log in logs %}
+      <li role="presentation" class="{{ 'active' if loop.last else '' }}">
+        <a href="#{{ loop.index }}" aria-controls="{{ loop.index }}" role="tab" data-toggle="tab">
+          {{ loop.index }}
+        </a>
+      </li>
+    {% endfor %}
+  </ul>
+  <div class="tab-content">
+    {% for log in logs %}
+      <div role="tabpanel" class="tab-pane {{ 'active' if loop.last else '' }}" id="{{ loop.index }}">
+        <pre id="attempt-{{ loop.index }}">{{ log }}</pre>
+      </div>
+    {% endfor %}
+  </div>
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/05e1861e/airflow/www_rbac/templates/airflow/traceback.html
----------------------------------------------------------------------
diff --git a/airflow/www_rbac/templates/airflow/traceback.html b/airflow/www_rbac/templates/airflow/traceback.html
new file mode 100644
index 0000000..41c9ed7
--- /dev/null
+++ b/airflow/www_rbac/templates/airflow/traceback.html
@@ -0,0 +1,33 @@
+{# 
+  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.
+
+#}
+<html>
+  <head>
+    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
+  </head>
+  <body>
+    <div class="container">
+      <h1> Ooops. </h1>
+      <div>
+          <pre>
+{{ nukular }}Node: {{ hostname }}
+-------------------------------------------------------------------------------
+{{ info }}</pre>
+      </div>
+    </div>
+  </body>
+</html>

http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/05e1861e/airflow/www_rbac/templates/airflow/tree.html
----------------------------------------------------------------------
diff --git a/airflow/www_rbac/templates/airflow/tree.html b/airflow/www_rbac/templates/airflow/tree.html
new file mode 100644
index 0000000..1028861
--- /dev/null
+++ b/airflow/www_rbac/templates/airflow/tree.html
@@ -0,0 +1,381 @@
+{#
+  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.
+
+#}
+{% extends "airflow/dag.html" %}
+{% block title %}Airflow - DAGs{% endblock %}
+
+{% block head_css %}
+{{ super() }}
+<link rel="stylesheet" type="text/css"
+    href="{{ url_for('static', filename='tree.css') }}">
+<link href="{{ url_for('static', filename='appbuilder/daterangepicker/bootstrap-datepicker.css') }}" rel="stylesheet">
+{% endblock %}
+
+{% block content %}
+{{ super() }}
+<div style="float: left" class="form-inline">
+    <form method="get" style="float:left;">
+        Base date: {{ form.base_date(class_="form-control") }}
+        Number of runs: {{ form.num_runs(class_="form-control") }}
+        <input type="hidden" name="root" value="{{ root if root else '' }}">
+        <input type="hidden" value="{{ dag.dag_id }}" name="dag_id">
+        <input type="submit" value="Go" class="btn btn-default"
+        action="" method="get">
+        <input name="_csrf_token" type="hidden" value="{{ csrf_token() }}">
+    </form>
+</div>
+<div style="clear: both;"></div>
+<hr/>
+<div>
+    <div class="legend_item" style="border: none;">no status</div>
+    <div class="square" style="background: white;"></div>
+    <div class="legend_item" style="border: none;">queued</div>
+    <div class="square" style="background: grey;"></div>
+    <div class="legend_item" style="border: none;">retry</div>
+    <div class="square" style="background: gold;"></div>
+    <div class="legend_item" style="border: none;">skipped</div>
+    <div class="square" style="background: pink;"></div>
+    <div class="legend_item" style="border: none;">failed</div>
+    <div class="square" style="background: red;"></div>
+    <div class="legend_item" style="border: none;">running</div>
+    <div class="square" style="background: lime;"></div>
+    <div class="legend_item" style="border: none;">success</div>
+    <div class="square" style="background: green;"></div>
+    {% for op in operators %}
+        <div class="legend_circle" style="background:{{ op.ui_color }};">
+        </div>
+        <div class="legend_item" style="float:left;border-color:white;">{{ op.__name__ }}</div>
+    {% endfor %}
+    <div style="clear:both;"></div>
+</div>
+<hr/>
+<div id="svg_container">
+  <img id='loading' width="50"
+    src="{{ url_for('static', filename='loading.gif') }}">
+    <svg class='tree' width="100%">
+        <filter id="blur-effect-1">
+            <feGaussianBlur stdDeviation="3" />
+        </filter>
+    </svg>
+</div>
+{% endblock %}
+
+{% block tail %}
+  {{ super() }}
+  <script src="{{ url_for('static', filename='d3.v3.min.js') }}"></script>
+  <script>
+$('span.status_square').tooltip({html: true});
+
+var data = {{ data|safe }};
+var barHeight = 20;
+var axisHeight = 40;
+var square_x = 500;
+var square_size = 10;
+var square_spacing = 2;
+var margin = {top: barHeight/2 + axisHeight, right: 0, bottom: 0, left: barHeight/2},
+    width = 960 - margin.left - margin.right,
+    barWidth = width * 0.9;
+
+var i = 0,
+    duration = 400,
+    root;
+
+var tree = d3.layout.tree().nodeSize([0, 25]);
+var nodes = tree.nodes(data);
+var nodeobj = {};
+for (i=0; i<nodes.length; i++) {
+    node = nodes[i];
+    nodeobj[node.name] = node;
+}
+
+var diagonal = d3.svg.diagonal()
+    .projection(function(d) { return [d.y, d.x]; });
+
+var svg = d3.select("svg")
+    //.attr("width", width + margin.left + margin.right)
+  .append("g")
+  .attr("class", "level")
+    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
+
+    data.x0 = 0;
+    data.y0 = 0;
+
+  if (nodes.length == 1)
+    var base_node = nodes[0];
+  else
+    var base_node = nodes[1];
+
+  var num_square = base_node.instances.length;
+  var extent = d3.extent(base_node.instances, function(d,i) {
+    return new Date(d.execution_date);
+  });
+  var xScale = d3.time.scale()
+  .domain(extent)
+  .range([
+    square_size/2,
+    (num_square * square_size) + ((num_square-1) * square_spacing) - (square_size/2)
+  ]);
+
+  d3.select("svg")
+  .insert("g")
+  .attr("transform",
+    "translate("+ (square_x + margin.left) +", " + axisHeight + ")")
+  .attr("class", "axis").call(
+    d3.svg.axis()
+    .scale(xScale)
+    .orient("top")
+    .ticks(2)
+  )
+  .selectAll("text")
+  .attr("transform", "rotate(-30)")
+  .style("text-anchor", "start");
+
+  function node_class(d) {
+        var sclass = "node";
+        if (d.children === undefined && d._children === undefined)
+          sclass += " leaf";
+        else {
+          sclass += " parent";
+          if (d.children === undefined)
+            sclass += " collapsed"
+          else
+            sclass += " expanded"
+        }
+        return sclass;
+  }
+
+update(root = data);
+function update(source) {
+
+  // Compute the flattened node list. TODO use d3.layout.hierarchy.
+  var nodes = tree.nodes(root);
+
+  var height = Math.max(500, nodes.length * barHeight + margin.top + margin.bottom);
+  var width = square_x + (num_square * (square_size + square_spacing)) + margin.left + margin.right + 50;
+  d3.select("svg").transition()
+      .duration(duration)
+      .attr("height", height)
+      .attr("width", width);
+
+  d3.select(self.frameElement).transition()
+      .duration(duration)
+      .style("height", height + "px");
+
+  // Compute the "layout".
+  nodes.forEach(function(n, i) {
+    n.x = i * barHeight;
+  });
+
+  // Update the nodes…
+  var node = svg.selectAll("g.node")
+      .data(nodes, function(d) { return d.id || (d.id = ++i); });
+
+  var nodeEnter = node.enter().append("g")
+  .attr("class", node_class)
+  .attr("transform", function(d) {
+    return "translate(" + source.y0 + "," + source.x0 + ")";
+  })
+  .style("opacity", 1e-6);
+
+  nodeEnter.append("circle")
+      .attr("r", (barHeight / 3))
+      .attr("class", "task")
+      .attr("data-toggle", "tooltip")
+      .attr("title", function(d){
+        var tt = "";
+        if (d.operator != undefined) {
+          tt += "operator: " + d.operator + "<br/>";
+          tt += "depends_on_past: " + d.depends_on_past + "<br/>";
+          tt += "upstream: " + d.num_dep + "<br/>";
+          tt += "retries: " + d.retries + "<br/>";
+          tt += "owner: " + d.owner + "<br/>";
+          tt += "start_date: " + d.start_date + "<br/>";
+          tt += "end_date: " + d.end_date + "<br/>";
+        }
+        return tt;
+      })
+      .attr("height", barHeight)
+      .attr("width", function(d, i) {return barWidth - d.y;})
+      .style("fill", function(d) {return d.ui_color;})
+      .attr("task_id", function(d){return d.name})
+      .on("click", toggles);
+
+  text = nodeEnter.append("text")
+      .attr("dy", 3.5)
+      .attr("dx", barHeight/2)
+      .text(function(d) { return d.name; });
+  {% if blur %}
+  text.attr("class", "blur");
+  {% endif %}
+
+  nodeEnter.append('g')
+      .attr("class", "stateboxes")
+      .attr("transform",
+        function(d, i) { return "translate(" + (square_x-d.y) + ",0)"; })
+      .selectAll("rect").data(function(d) { return d.instances; })
+      .enter()
+      .append('rect')
+      .on("click", function(d){
+        if(d.task_id === undefined)
+            call_modal_dag(d);
+        else if(nodeobj[d.task_id].operator=='SubDagOperator')
+            call_modal(d.task_id, d.execution_date, true);
+        else
+            call_modal(d.task_id, d.execution_date);
+      })
+      .attr("class", function(d) {return "state " + d.state})
+      .attr("data-toggle", "tooltip")
+      .attr("rx", function(d) {return (d.run_id != undefined)? "5": "0"})
+      .attr("ry", function(d) {return (d.run_id != undefined)? "5": "0"})
+      .style("shape-rendering", function(d) {return (d.run_id != undefined)? "auto": "crispEdges"})
+      .style("stroke-width", function(d) {return (d.run_id != undefined)? "2": "1"})
+      .style("stroke-opacity", function(d) {return d.external_trigger ? "0": "1"})
+      .attr("title", function(d){
+        s =  "Task_id: " + d.task_id + "<br>";
+        s += "Run: " + d.execution_date + "<br>";
+        if(d.run_id != undefined){
+          s += "run_id: <nobr>" + d.run_id + "</nobr><br>";
+        }
+        s += "Operator: " + d.operator + "<br>"
+        if(d.start_date != undefined){
+          s += "Started: " + d.start_date + "<br>";
+          s += "Ended: " + d.end_date + "<br>";
+          s += "Duration: " + d.duration + "<br>";
+          s += "State: " + d.state + "<br>";
+        }
+        return s;
+      })
+      .attr('x', function(d, i) {return (i*(square_size+square_spacing));})
+      .attr('y', -square_size/2)
+      .attr('width', 10)
+      .attr('height', 10)
+      .on('mouseover', function(d,i) {
+        d3.select(this).transition()
+          .style('stroke-width', 3)
+       })
+      .on('mouseout', function(d,i) {
+        d3.select(this).transition()
+          .style("stroke-width", function(d) {return (d.run_id != undefined)? "2": "1"})
+       }) ;
+
+
+  // Transition nodes to their new position.
+  nodeEnter.transition()
+      .duration(duration)
+      .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
+      .style("opacity", 1);
+
+  node.transition()
+      .duration(duration)
+      .attr("class", node_class)
+      .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
+      .style("opacity", 1);
+
+  // Transition exiting nodes to the parent's new position.
+  node.exit().transition()
+      .duration(duration)
+      .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; })
+      .style("opacity", 1e-6)
+      .remove();
+
+  // Update the links…
+  var link = svg.selectAll("path.link")
+      .data(tree.links(nodes), function(d) { return d.target.id; });
+
+  // Enter any new links at the parent's previous position.
+  link.enter().insert("path", "g")
+      .attr("class", "link")
+      .attr("d", function(d) {
+        var o = {x: source.x0, y: source.y0};
+        return diagonal({source: o, target: o});
+      })
+    .transition()
+      .duration(duration)
+      .attr("d", diagonal);
+
+  // Transition links to their new position.
+  link.transition()
+      .duration(duration)
+      .attr("d", diagonal);
+
+  // Transition exiting nodes to the parent's new position.
+  link.exit().transition()
+      .duration(duration)
+      .attr("d", function(d) {
+        var o = {x: source.x, y: source.y};
+        return diagonal({source: o, target: o});
+      })
+      .remove();
+
+  // Stash the old positions for transition.
+  nodes.forEach(function(d) {
+    d.x0 = d.x;
+    d.y0 = d.y;
+  });
+
+  $('#loading').remove()
+}
+
+function set_tooltip(){
+  $("rect.state").tooltip({
+    html: true,
+    container: "body",
+  });
+  $("circle.task").tooltip({
+    html: true,
+    container: "body",
+  });
+
+}
+function toggles(clicked_d) {
+    // Collapse nodes with the same task id
+    d3.selectAll("[task_id='" + clicked_d.name + "']").each(function(d){
+      if(clicked_d != d && d.children) {
+          d._children = d.children;
+          d.children = null;
+        update(d);
+      }
+    });
+
+    // Toggle clicked node
+    if(clicked_d._children) {
+        clicked_d.children = clicked_d._children;
+        clicked_d._children = null;
+    } else {
+        clicked_d._children = clicked_d.children;
+        clicked_d.children = null;
+    }
+    update(clicked_d);
+    set_tooltip();
+}
+// Toggle children on click.
+function click(d) {
+  if (d.children || d._children){
+    if (d.children) {
+      d._children = d.children;
+      d.children = null;
+    } else {
+      d.children = d._children;
+      d._children = null;
+    }
+    update(d);
+    set_tooltip();
+  }
+}
+set_tooltip();
+  </script>
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/05e1861e/airflow/www_rbac/templates/airflow/variable_list.html
----------------------------------------------------------------------
diff --git a/airflow/www_rbac/templates/airflow/variable_list.html b/airflow/www_rbac/templates/airflow/variable_list.html
new file mode 100644
index 0000000..a732217
--- /dev/null
+++ b/airflow/www_rbac/templates/airflow/variable_list.html
@@ -0,0 +1,30 @@
+{#
+  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.
+
+#}
+{% extends 'appbuilder/general/model/list.html' %}
+
+{% block content %}
+  <form class="form-inline" action="{{ url_for('VariableModelView.varimport') }}" method=post enctype=multipart/form-data style="margin-top: 50px;">
+    {% if csrf_token %}
+    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
+    {% endif %}
+    <input class="form-control" type="file" name="file">
+    <input class="btn btn-default" type="submit" value="Import Variables"/>
+  </form>
+  <hr/>
+  {{ super() }}
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/05e1861e/airflow/www_rbac/templates/airflow/version.html
----------------------------------------------------------------------
diff --git a/airflow/www_rbac/templates/airflow/version.html b/airflow/www_rbac/templates/airflow/version.html
new file mode 100644
index 0000000..5da84fd
--- /dev/null
+++ b/airflow/www_rbac/templates/airflow/version.html
@@ -0,0 +1,30 @@
+{#
+#
+# Licensed 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.
+#
+#}
+{% extends "airflow/master.html" %}
+
+{% block content %}
+    {{ super() }}
+    <h2>{{ title }}</h2>
+	{% set version_label = 'Version' %}
+    {% if airflow_version %}
+        <h4>{{ version_label }} : <a href="https://pypi.python.org/pypi/apache-airflow/{{ airflow_version }}">{{ airflow_version }}</a></h4>
+    {% else %}
+        <h4>{{ version_label }} : Not Available</h4>
+    {% endif %}
+	<h4>Git Version :{% if git_version %} {{ git_version }} {% else %} Not Available {% endif %}</h4>
+    <hr>
+
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/05e1861e/airflow/www_rbac/templates/airflow/xcom.html
----------------------------------------------------------------------
diff --git a/airflow/www_rbac/templates/airflow/xcom.html b/airflow/www_rbac/templates/airflow/xcom.html
new file mode 100644
index 0000000..a8f7f21
--- /dev/null
+++ b/airflow/www_rbac/templates/airflow/xcom.html
@@ -0,0 +1,37 @@
+{#
+#
+# Licensed 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.
+#
+#}
+{% extends "airflow/task_instance.html" %}
+{% block title %}Airflow - DAGs{% endblock %}
+
+{% block content %}
+    {{ super() }}
+    <h4>{{ title }}</h4>
+    <div>
+        <table class="table table-striped table-bordered">
+            <tr>
+                <th>Key</th>
+                <th>Value</th>
+            </tr>
+            {% for attr, value in attributes %}
+                <tr>
+                    <td>{{ attr }}</td>
+                    <td class='code'>{{ value }}</td>
+                </tr>
+            {% endfor %}
+        </table>
+        {{ html_code|safe }}
+    </div>
+{% endblock %}

http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/05e1861e/airflow/www_rbac/templates/appbuilder/baselayout.html
----------------------------------------------------------------------
diff --git a/airflow/www_rbac/templates/appbuilder/baselayout.html b/airflow/www_rbac/templates/appbuilder/baselayout.html
new file mode 100644
index 0000000..eeb3d5c
--- /dev/null
+++ b/airflow/www_rbac/templates/appbuilder/baselayout.html
@@ -0,0 +1,84 @@
+{#
+  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.
+
+#}
+{% extends 'appbuilder/init.html' %}
+{% import 'appbuilder/baselib.html' as baselib %}
+
+{% block head_css %}
+  {{ super() }}
+  <link href="{{ url_for('static', filename='main.css') }}" rel="stylesheet">
+  <link href="{{ url_for('static', filename='bootstrap-theme.css') }}" rel="stylesheet">
+  <link rel="icon" type="image/png" href="{{ url_for("static", filename="pin_30.png") }}">
+{% endblock %}
+
+
+{% block body %}
+    {% include 'appbuilder/general/confirm.html' %}
+    {% include 'appbuilder/general/alert.html' %}
+    {% block navbar %}
+        <header class="top" role="header">
+        {% include 'appbuilder/navbar.html' %}
+        </header>
+    {% endblock %}
+   
+
+    <div class="container">
+      <div class="row">
+          {% block messages %}
+            {% include 'appbuilder/flash.html' %}
+          {% endblock %}
+          {% block content %}
+          {% endblock %}
+      </div>
+    </div>
+
+    {% block footer %}
+        <footer>
+        <div class="img-rounded nav-fixed-bottom">
+            <div class="container">
+                {% include 'appbuilder/footer.html' %}
+            </div>
+        </div>
+        </footer>
+    {% endblock %}
+{% endblock %}
+
+
+{% block tail_js %}
+{{ super() }}
+<script src="{{ url_for('static', filename='jqClock.min.js') }}" type="text/javascript"></script>
+<script>
+    x = new Date()
+    var UTCseconds = (x.getTime() + x.getTimezoneOffset()*60*1000);
+    $("#clock").clock({
+        "dateFormat":"Y-m-d ",
+        "timeFormat":"H:i:s %UTC%",
+        "timestamp":UTCseconds
+    }).click(function(){
+        alert('{{ hostname }}');
+    });
+    $('span').tooltip();
+
+    $.ajaxSetup({
+      beforeSend: function(xhr, settings) {
+        if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
+          xhr.setRequestHeader("X-CSRFToken", "{{ csrf_token() }}");
+        }
+      }
+    });
+</script>
+{% endblock %}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/05e1861e/airflow/www_rbac/templates/appbuilder/index.html
----------------------------------------------------------------------
diff --git a/airflow/www_rbac/templates/appbuilder/index.html b/airflow/www_rbac/templates/appbuilder/index.html
new file mode 100644
index 0000000..0384d5f
--- /dev/null
+++ b/airflow/www_rbac/templates/appbuilder/index.html
@@ -0,0 +1,18 @@
+{#
+  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.
+
+#}
+{% extends "airflow/dag.html" %}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/05e1861e/airflow/www_rbac/templates/appbuilder/navbar.html
----------------------------------------------------------------------
diff --git a/airflow/www_rbac/templates/appbuilder/navbar.html b/airflow/www_rbac/templates/appbuilder/navbar.html
new file mode 100644
index 0000000..d2c9e1d
--- /dev/null
+++ b/airflow/www_rbac/templates/appbuilder/navbar.html
@@ -0,0 +1,47 @@
+{#
+  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.
+
+#}
+{% set menu = appbuilder.menu %}
+{% set languages = appbuilder.languages %}
+
+<div class="navbar navbar-inverse navbar-fixed-top {{menu.extra_classes}}" role="navigation">
+   <div class="container">
+        <div class="navbar-header">
+            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
+                <span class="icon-bar"></span>
+                <span class="icon-bar"></span>
+                <span class="icon-bar"></span>
+            </button>
+            <a class="navbar-brand" rel="home" href="{{appbuilder.get_url_for_index}}" style="cursor: pointer;">
+              <img style="float: left; width:35px; margin-top: -7px;"
+                   src="{{ url_for("static", filename="pin_100.png") }}"
+                   title="{{ current_user.username }}">
+              <span>
+                Airflow
+              </span>
+          </a>
+        </div>
+        <div class="navbar-collapse collapse">
+            <ul class="nav navbar-nav">
+                {% include 'appbuilder/navbar_menu.html' %}
+            </ul>
+            <ul class="nav navbar-nav navbar-right">
+                {% include 'appbuilder/navbar_right.html' %}
+            </ul>
+        </div>
+   </div>
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/05e1861e/airflow/www_rbac/templates/appbuilder/navbar_menu.html
----------------------------------------------------------------------
diff --git a/airflow/www_rbac/templates/appbuilder/navbar_menu.html b/airflow/www_rbac/templates/appbuilder/navbar_menu.html
new file mode 100644
index 0000000..5661e89
--- /dev/null
+++ b/airflow/www_rbac/templates/appbuilder/navbar_menu.html
@@ -0,0 +1,57 @@
+{#
+  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.
+
+#}
+{% macro menu_item(item) %}
+    <a tabindex="-1" href="{{item.get_url()}}">
+       {% if item.icon %}
+        <i class="fa fa-fw {{item.icon}}"></i>&nbsp;
+    {% endif %}
+    {{_(item.label)}}</a>
+{% endmacro %}
+
+<li class="dropdown"><a href="/">DAGs</a></li>
+
+{% for item1 in menu.get_list() %}
+    {% if item1 | is_menu_visible %}
+        {% if item1.childs %}
+            <li class="dropdown">
+            <a class="dropdown-toggle" data-toggle="dropdown" href="javascript:void(0)">
+            {% if item1.icon %}
+                <i class="fa {{item1.icon}}"></i>&nbsp;
+            {% endif %}
+            {{_(item1.label)}}<b class="caret"></b></a>
+            <ul class="dropdown-menu">
+            {% for item2 in item1.childs %}
+                {% if item2 %}
+                    {% if item2.name == '-' %}
+                        {% if not loop.last %}
+                          <li class="divider"></li>
+                        {% endif %}
+                    {% elif item2 | is_menu_visible %}
+                        <li>{{ menu_item(item2) }}</li>
+                    {% endif %}
+                {% endif %}
+            {% endfor %}
+            </ul></li>
+        {% else %}
+            <li>
+                {{ menu_item(item1) }}
+            </li>
+        {% endif %}
+    {% endif %}
+{% endfor %}
+

http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/05e1861e/airflow/www_rbac/templates/appbuilder/navbar_right.html
----------------------------------------------------------------------
diff --git a/airflow/www_rbac/templates/appbuilder/navbar_right.html b/airflow/www_rbac/templates/appbuilder/navbar_right.html
new file mode 100644
index 0000000..bf5aa43
--- /dev/null
+++ b/airflow/www_rbac/templates/appbuilder/navbar_right.html
@@ -0,0 +1,64 @@
+{#
+  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.
+
+#}
+{% macro locale_menu(languages) %}
+{% set locale = session['locale'] %}
+{% if not locale %}
+    {% set locale = 'en' %}
+{% endif %}
+
+
+
+<li class="dropdown">
+    <a class="dropdown-toggle" data-toggle="dropdown" href="javascript:void(0)">
+       <div class="f16"><i class="flag {{languages[locale].get('flag')}}"></i><b class="caret"></b>
+       </div>
+    </a>
+    {% if languages.keys()|length > 1 %}
+    <ul class="dropdown-menu">
+    <li class="dropdown">
+        {% for lang in languages %}
+            {% if lang != locale %}
+                <a tabindex="-1" href="{{appbuilder.get_url_for_locale(lang)}}">
+                  <div class="f16"><i class="flag {{languages[lang].get('flag')}}"></i> - {{languages[lang].get('name')}}
+                </div></a>
+                {% endif %}
+            {% endfor %}
+        </li>
+        </ul>
+    {% endif %}
+</li>
+{% endmacro %}
+
+<!-- clock -->
+<li><a id="clock"></a></li>
+
+{% if not current_user.is_anonymous() %}
+    <li class="dropdown">
+        <a class="dropdown-toggle" data-toggle="dropdown" href="#">
+           <span class="fa fa-user"></span> {{g.user.get_full_name()}}<b class="caret"></b>
+        </a>
+        <ul class="dropdown-menu">
+            <li><a href="{{appbuilder.get_url_for_userinfo}}"><span class="fa fa-fw fa-user"></span>{{_("Profile")}}</a></li>
+            <li><a href="{{appbuilder.get_url_for_logout}}"><span class="fa fa-fw fa-sign-out"></span>{{_("Logout")}}</a></li>
+        </ul>
+    </li>
+{% else %}
+    <li><a href="{{appbuilder.get_url_for_login}}">
+    <i class="fa fa-fw fa-sign-in"></i>{{_("Login")}}</a></li>
+{% endif %}
+

http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/05e1861e/airflow/www_rbac/utils.py
----------------------------------------------------------------------
diff --git a/airflow/www_rbac/utils.py b/airflow/www_rbac/utils.py
new file mode 100644
index 0000000..e15168d
--- /dev/null
+++ b/airflow/www_rbac/utils.py
@@ -0,0 +1,350 @@
+# -*- coding: utf-8 -*-
+#
+# Licensed 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 future import standard_library  # noqa
+standard_library.install_aliases()  # noqa
+
+import inspect
+import json
+import time
+import wtforms
+import bleach
+import markdown
+
+from builtins import str
+from past.builtins import basestring
+
+from pygments import highlight, lexers
+from pygments.formatters import HtmlFormatter
+from flask import request, Response, Markup, url_for
+from airflow import configuration
+from airflow.models import BaseOperator
+from airflow.operators.subdag_operator import SubDagOperator
+from airflow.utils import timezone
+from airflow.utils.json import AirflowJsonEncoder
+from airflow.utils.state import State
+
+AUTHENTICATE = configuration.getboolean('webserver', 'AUTHENTICATE')
+
+DEFAULT_SENSITIVE_VARIABLE_FIELDS = (
+    'password',
+    'secret',
+    'passwd',
+    'authorization',
+    'api_key',
+    'apikey',
+    'access_token',
+)
+
+
+def should_hide_value_for_key(key_name):
+    return any(s in key_name.lower() for s in DEFAULT_SENSITIVE_VARIABLE_FIELDS) \
+        and configuration.getboolean('admin', 'hide_sensitive_variable_fields')
+
+
+def get_params(**kwargs):
+    params = []
+    for k, v in kwargs.items():
+        if k == 'showPaused':
+            # True is default or None
+            if v or v is None:
+                continue
+            params.append('{}={}'.format(k, v))
+        elif v:
+            params.append('{}={}'.format(k, v))
+    params = sorted(params, key=lambda x: x.split('=')[0])
+    return '&'.join(params)
+
+
+def generate_pages(current_page, num_of_pages,
+                   search=None, showPaused=None, window=7):
+    """
+    Generates the HTML for a paging component using a similar logic to the paging
+    auto-generated by Flask managed views. The paging component defines a number of
+    pages visible in the pager (window) and once the user goes to a page beyond the
+    largest visible, it would scroll to the right the page numbers and keeps the
+    current one in the middle of the pager component. When in the last pages,
+    the pages won't scroll and just keep moving until the last page. Pager also contains
+    <first, previous, ..., next, last> pages.
+    This component takes into account custom parameters such as search and showPaused,
+    which could be added to the pages link in order to maintain the state between
+    client and server. It also allows to make a bookmark on a specific paging state.
+    :param current_page:
+        the current page number, 0-indexed
+    :param num_of_pages:
+        the total number of pages
+    :param search:
+        the search query string, if any
+    :param showPaused:
+        false if paused dags will be hidden, otherwise true to show them
+    :param window:
+        the number of pages to be shown in the paging component (7 default)
+    :return:
+        the HTML string of the paging component
+    """
+
+    void_link = 'javascript:void(0)'
+    first_node = """<li class="paginate_button {disabled}" id="dags_first">
+    <a href="{href_link}" aria-controls="dags" data-dt-idx="0" tabindex="0">&laquo;</a>
+</li>"""
+
+    previous_node = """<li class="paginate_button previous {disabled}" id="dags_previous">
+    <a href="{href_link}" aria-controls="dags" data-dt-idx="0" tabindex="0">&lt;</a>
+</li>"""
+
+    next_node = """<li class="paginate_button next {disabled}" id="dags_next">
+    <a href="{href_link}" aria-controls="dags" data-dt-idx="3" tabindex="0">&gt;</a>
+</li>"""
+
+    last_node = """<li class="paginate_button {disabled}" id="dags_last">
+    <a href="{href_link}" aria-controls="dags" data-dt-idx="3" tabindex="0">&raquo;</a>
+</li>"""
+
+    page_node = """<li class="paginate_button {is_active}">
+    <a href="{href_link}" aria-controls="dags" data-dt-idx="2" tabindex="0">{page_num}</a>
+</li>"""
+
+    output = ['<ul class="pagination" style="margin-top:0px;">']
+
+    is_disabled = 'disabled' if current_page <= 0 else ''
+    output.append(first_node.format(href_link="?{}"
+                                    .format(get_params(page=0,
+                                                       search=search,
+                                                       showPaused=showPaused)),
+                                    disabled=is_disabled))
+
+    page_link = void_link
+    if current_page > 0:
+        page_link = '?{}'.format(get_params(page=(current_page - 1),
+                                            search=search,
+                                            showPaused=showPaused))
+
+    output.append(previous_node.format(href_link=page_link,
+                                       disabled=is_disabled))
+
+    mid = int(window / 2)
+    last_page = num_of_pages - 1
+
+    if current_page <= mid or num_of_pages < window:
+        pages = [i for i in range(0, min(num_of_pages, window))]
+    elif mid < current_page < last_page - mid:
+        pages = [i for i in range(current_page - mid, current_page + mid + 1)]
+    else:
+        pages = [i for i in range(num_of_pages - window, last_page + 1)]
+
+    def is_current(current, page):
+        return page == current
+
+    for page in pages:
+        vals = {
+            'is_active': 'active' if is_current(current_page, page) else '',
+            'href_link': void_link if is_current(current_page, page)
+                         else '?{}'.format(get_params(page=page,
+                                                      search=search,
+                                                      showPaused=showPaused)),
+            'page_num': page + 1
+        }
+        output.append(page_node.format(**vals))
+
+    is_disabled = 'disabled' if current_page >= num_of_pages - 1 else ''
+
+    page_link = (void_link if current_page >= num_of_pages - 1
+                 else '?{}'.format(get_params(page=current_page + 1,
+                                              search=search,
+                                              showPaused=showPaused)))
+
+    output.append(next_node.format(href_link=page_link, disabled=is_disabled))
+    output.append(last_node.format(href_link="?{}"
+                                   .format(get_params(page=last_page,
+                                                      search=search,
+                                                      showPaused=showPaused)),
+                                   disabled=is_disabled))
+
+    output.append('</ul>')
+
+    return wtforms.widgets.core.HTMLString('\n'.join(output))
+
+
+def epoch(dttm):
+    """Returns an epoch-type date"""
+    return int(time.mktime(dttm.timetuple())) * 1000,
+
+
+def json_response(obj):
+    """
+    returns a json response from a json serializable python object
+    """
+    return Response(
+        response=json.dumps(
+            obj, indent=4, cls=AirflowJsonEncoder),
+        status=200,
+        mimetype="application/json")
+
+
+def make_cache_key(*args, **kwargs):
+    '''
+    Used by cache to get a unique key per URL
+    '''
+    path = request.path
+    args = str(hash(frozenset(request.args.items())))
+    return (path + args).encode('ascii', 'ignore')
+
+
+def task_instance_link(attr):
+    dag_id = bleach.clean(attr.get('dag_id')) if attr.get('dag_id') else None
+    task_id = bleach.clean(attr.get('task_id')) if attr.get('task_id') else None
+    execution_date = attr.get('execution_date')
+    url = url_for(
+        'Airflow.task',
+        dag_id=dag_id,
+        task_id=task_id,
+        execution_date=execution_date.isoformat())
+    url_root = url_for(
+        'Airflow.graph',
+        dag_id=dag_id,
+        root=task_id,
+        execution_date=execution_date.isoformat())
+    return Markup(
+        """
+        <span style="white-space: nowrap;">
+        <a href="{url}">{task_id}</a>
+        <a href="{url_root}" title="Filter on this task and upstream">
+        <span class="glyphicon glyphicon-filter" style="margin-left: 0px;"
+            aria-hidden="true"></span>
+        </a>
+        </span>
+        """.format(**locals()))
+
+
+def state_token(state):
+    color = State.color(state)
+    return Markup(
+        '<span class="label" style="background-color:{color};">'
+        '{state}</span>'.format(**locals()))
+
+
+def state_f(attr):
+    state = attr.get('state')
+    return state_token(state)
+
+
+def nobr_f(attr_name):
+    def nobr(attr):
+        f = attr.get(attr_name)
+        return Markup("<nobr>{}</nobr>".format(f))
+    return nobr
+
+
+def datetime_f(attr_name):
+    def dt(attr):
+        f = attr.get(attr_name)
+        f = f.isoformat() if f else ''
+        if timezone.utcnow().isoformat()[:4] == f[:4]:
+            f = f[5:]
+        return Markup("<nobr>{}</nobr>".format(f))
+    return dt
+
+
+def dag_link(attr):
+    dag_id = bleach.clean(attr.get('dag_id')) if attr.get('dag_id') else None
+    execution_date = attr.get('execution_date')
+    url = url_for(
+        'Airflow.graph',
+        dag_id=dag_id,
+        execution_date=execution_date)
+    return Markup(
+        '<a href="{}">{}</a>'.format(url, dag_id))
+
+
+def dag_run_link(attr):
+    dag_id = bleach.clean(attr.get('dag_id')) if attr.get('dag_id') else None
+    run_id = bleach.clean(attr.get('run_id')) if attr.get('run_id') else None
+    execution_date = attr.get('execution_date')
+    url = url_for(
+        'Airflow.graph',
+        dag_id=dag_id,
+        run_id=run_id,
+        execution_date=execution_date)
+    return Markup(
+        '<a href="{url}">{run_id}</a>'.format(**locals()))
+
+
+def pygment_html_render(s, lexer=lexers.TextLexer):
+    return highlight(
+        s,
+        lexer(),
+        HtmlFormatter(linenos=True),
+    )
+
+
+def render(obj, lexer):
+    out = ""
+    if isinstance(obj, basestring):
+        out += pygment_html_render(obj, lexer)
+    elif isinstance(obj, (tuple, list)):
+        for i, s in enumerate(obj):
+            out += "<div>List item #{}</div>".format(i)
+            out += "<div>" + pygment_html_render(s, lexer) + "</div>"
+    elif isinstance(obj, dict):
+        for k, v in obj.items():
+            out += '<div>Dict item "{}"</div>'.format(k)
+            out += "<div>" + pygment_html_render(v, lexer) + "</div>"
+    return out
+
+
+def wrapped_markdown(s):
+    return '<div class="rich_doc">' + markdown.markdown(s) + "</div>"
+
+
+def get_attr_renderer():
+    attr_renderer = {
+        'bash_command': lambda x: render(x, lexers.BashLexer),
+        'hql': lambda x: render(x, lexers.SqlLexer),
+        'sql': lambda x: render(x, lexers.SqlLexer),
+        'doc': lambda x: render(x, lexers.TextLexer),
+        'doc_json': lambda x: render(x, lexers.JsonLexer),
+        'doc_rst': lambda x: render(x, lexers.RstLexer),
+        'doc_yaml': lambda x: render(x, lexers.YamlLexer),
+        'doc_md': wrapped_markdown,
+        'python_callable': lambda x: render(
+            inspect.getsource(x), lexers.PythonLexer),
+    }
+    return attr_renderer
+
+
+def recurse_tasks(tasks, task_ids, dag_ids, task_id_to_dag):
+    if isinstance(tasks, list):
+        for task in tasks:
+            recurse_tasks(task, task_ids, dag_ids, task_id_to_dag)
+        return
+    if isinstance(tasks, SubDagOperator):
+        subtasks = tasks.subdag.tasks
+        dag_ids.append(tasks.subdag.dag_id)
+        for subtask in subtasks:
+            if subtask.task_id not in task_ids:
+                task_ids.append(subtask.task_id)
+                task_id_to_dag[subtask.task_id] = tasks.subdag
+        recurse_tasks(subtasks, task_ids, dag_ids, task_id_to_dag)
+    if isinstance(tasks, BaseOperator):
+        task_id_to_dag[tasks.task_id] = tasks.dag
+
+
+def get_chart_height(dag):
+    """
+    TODO(aoen): See [AIRFLOW-1263] We use the number of tasks in the DAG as a heuristic to
+    approximate the size of generated chart (otherwise the charts are tiny and unreadable
+    when DAGs have a large number of tasks). Ideally nvd3 should allow for dynamic-height
+    charts, that is charts that take up space based on the size of the components within.
+    """
+    return 600 + len(dag.tasks) * 10

http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/05e1861e/airflow/www_rbac/validators.py
----------------------------------------------------------------------
diff --git a/airflow/www_rbac/validators.py b/airflow/www_rbac/validators.py
new file mode 100644
index 0000000..63557b4
--- /dev/null
+++ b/airflow/www_rbac/validators.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+#
+# Licensed 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 wtforms.validators import EqualTo
+from wtforms.validators import ValidationError
+
+
+class GreaterEqualThan(EqualTo):
+    """Compares the values of two fields.
+
+    :param fieldname:
+        The name of the other field to compare to.
+    :param message:
+        Error message to raise in case of a validation error. Can be
+        interpolated with `%(other_label)s` and `%(other_name)s` to provide a
+        more helpful error.
+    """
+
+    def __call__(self, form, field):
+        try:
+            other = form[self.fieldname]
+        except KeyError:
+            raise ValidationError(
+                field.gettext("Invalid field name '%s'." % self.fieldname)
+            )
+
+        if field.data is None or other.data is None:
+            return
+
+        if field.data < other.data:
+            d = {
+                'other_label':
+                    hasattr(other, 'label') and other.label.text or self.fieldname,
+                'other_name': self.fieldname,
+            }
+            message = self.message
+            if message is None:
+                message = field.gettext('Field must be greater than or equal '
+                                        'to %(other_label)s.' % d)
+            else:
+                message = message % d
+
+            raise ValidationError(message)