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)