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>
+ {% 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>
+ {% 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">«</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"><</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">></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">»</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)