Repository: incubator-airflow Updated Branches: refs/heads/master 7c0f8373f -> 004272b15
http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/0a460081/airflow/www/templates/airflow/chart.html ---------------------------------------------------------------------- diff --git a/airflow/www/templates/airflow/chart.html b/airflow/www/templates/airflow/chart.html index 1901af5..91b8be1 100644 --- a/airflow/www/templates/airflow/chart.html +++ b/airflow/www/templates/airflow/chart.html @@ -21,54 +21,21 @@ <input name="_csrf_token" type="hidden" value="{{ csrf_token() }}"> </form> </div> -<div style="clear: both;"></div> +<div style="clear: both;">{{ chart |safe }}</div> <hr/> {% endblock %} {% block tail %} {{ super() }} - <script src="{{ url_for('static', filename='d3.v3.min.js') }}"></script> - <script src="{{ url_for('static', filename='highcharts.js') }}"></script> - <script src="/ck/static/chartkick.js"></script> <script> // We blur task_ids in demo mode $( document ).ready(function() { - if ("{{ demo_mode }}" == "True") { - d3.select("svg") - .append("filter") - .attr("id", "blur-effect-1") - .append("feGaussianBlur") - .attr("stdDeviation", 3); - d3.selectAll("g.highcharts-legend-item text").style("filter", "url(#blur-effect-1)"); - } - $('#uncheck').click(function(){ - chart = Highcharts.charts[0]; - $(chart.series).each(function(){ - this.setVisible(false, false); - }); - chart.redraw(); - }); - $('#check').click(function(){ - chart = Highcharts.charts[0]; - $(chart.series).each(function(){ - this.setVisible(true, false); - }); - chart.redraw(); - }); }); </script> - <div class="container"> - {% line_chart data with height=height library=chart_options%} - <div class="text-center"> - <button class="btn" id="uncheck">Hide all series</button> - <button class="btn" id="check">Show all series</button> - </div> - </div> + <div class="container"></div> - <script src="{{ url_for('static', filename='d3.v3.min.js') }}"></script> <script src="{{ admin_static.url( filename='vendor/bootstrap-daterangepicker/daterangepicker.js') }}"></script> - <script src="{{ admin_static.url(filename='admin/js/form-1.0.0.js') }}"></script> <script> {% endblock %} http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/0a460081/airflow/www/templates/airflow/dag.html ---------------------------------------------------------------------- diff --git a/airflow/www/templates/airflow/dag.html b/airflow/www/templates/airflow/dag.html index 22b312e..89ce2d6 100644 --- a/airflow/www/templates/airflow/dag.html +++ b/airflow/www/templates/airflow/dag.html @@ -4,8 +4,8 @@ {% block title %}Airflow - DAG {{ dag.dag_id }}{% endblock %} {% block head_css %} - {{ super() }} {{ lib.form_css() }} + {{ super() }} {% endblock %} {% block body %} @@ -186,8 +186,8 @@ </div> {% endblock %} {% block tail %} - {{ super() }} {{ lib.form_js() }} + {{ super() }} <script> function updateQueryStringParameter(uri, key, value) { var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i"); http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/0a460081/airflow/www/templates/airflow/gantt.html ---------------------------------------------------------------------- diff --git a/airflow/www/templates/airflow/gantt.html b/airflow/www/templates/airflow/gantt.html index ada2bc8..cd58cc9 100644 --- a/airflow/www/templates/airflow/gantt.html +++ b/airflow/www/templates/airflow/gantt.html @@ -2,66 +2,48 @@ {% block head_css %} {{ super() }} -<link href="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker-bs2.css') }}" rel="stylesheet"> +<link href="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker-bs2.css') }}" rel="stylesheet"/> +<link type="text/css" href="{{ url_for('static', filename='gantt.css') }}" rel="stylesheet" /> +<link type="text/css" href="{{ url_for('static', filename='tree.css') }}" rel="stylesheet" /> {% endblock %} {% block body %} {{ super() }} <form method="get"> - <div class="form-inline"> - Run:<input type="hidden" value="{{ dag.dag_id }}" name="dag_id"> - {{ form.execution_date(class_="form-control") | safe }} - <input type="submit" value="Go" class="btn btn-default" action="" method="get"> - <input name="_csrf_token" type="hidden" value="{{ csrf_token() }}"> - </div> + <div class="form-inline"> + Run:<input type="hidden" value="{{ dag.dag_id }}" name="dag_id"> + {{ form.execution_date(class_="form-control") | safe }} + <input type="submit" value="Go" class="btn btn-default" action="" method="get"> + <input name="_csrf_token" type="hidden" value="{{ csrf_token() }}"> + </div> </form> -<div id="container"></div> +<div style="clear: both;"></div> +<div class="container"> + <div class="gantt"></div> +</div> {% endblock %} {% block tail %} - {{ super() }} - <script src="{{ url_for('static', filename='d3.v3.min.js') }}"></script> - <script src="{{ url_for('static', filename='highcharts.js') }}"></script> - <script src="{{ url_for('static', filename='highcharts-more.js') }}"> - </script> - <script src="{{ admin_static.url( - filename='vendor/bootstrap-daterangepicker/daterangepicker.js') }}"></script> - <script src="{{ admin_static.url(filename='admin/js/form-1.0.0.js') }}"></script> - <script> - $( document ).ready(function() { - execution_date = '{{ execution_date }}'; - hc = {{ hc|safe }}; - hc.plotOptions.series.point = { - events: { - click: function(p){ - call_modal(this.category, execution_date); - } - } - }; - - hc.tooltip = { - formatter: function() { - duration = new Date(this.point.high - this.point.low); - return "From " + - Highcharts.dateFormat('%H:%M:%S', new Date(this.point.low)) + - " to " + - Highcharts.dateFormat('%H:%M:%S', new Date(this.point.high)) + - "<br>Duration: " + - Highcharts.dateFormat('%H:%M:%S', new Date(duration)); - } - } - $("#container").highcharts(hc); - - // We blur task_ids in demo mode - if ("{{ demo_mode }}" == "True") { - d3.select("svg") - .append("filter") - .attr("id", "blur-effect-1") - .append("feGaussianBlur") - .attr("stdDeviation", 3); - d3.selectAll("g.highcharts-xaxis-labels text").style("filter", "url(#blur-effect-1)"); - } - }); - </script> +{{ super() }} +<script src="{{ admin_static.url( + filename='vendor/bootstrap-daterangepicker/daterangepicker.js') }}"></script> +<script src="{{ url_for('static', filename='d3.v3.min.js') }}"></script> +<script src="{{ url_for('static', filename='d3.tip.v0.6.3.js') }}"></script>/ +<script src="{{ url_for('static', filename='gantt-chart-d3v2.js') }}"></script> +<script> + $( document ).ready(function() { + var dag_id = '{{ dag.dag_id }}'; + var task_id = ''; + var exection_date = ''; + data = {{ data |safe }}; + var gantt = d3.gantt() + .taskTypes(data.taskNames) + .taskStatus(data.taskStatus) + .height(data.height) + .selector('.gantt') + .tickFormat("%H:%M:%S"); + gantt(data.tasks); + }); +</script> {% endblock %} http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/0a460081/airflow/www/templates/airflow/highchart.html ---------------------------------------------------------------------- diff --git a/airflow/www/templates/airflow/highchart.html b/airflow/www/templates/airflow/highchart.html deleted file mode 100644 index 728caca..0000000 --- a/airflow/www/templates/airflow/highchart.html +++ /dev/null @@ -1,183 +0,0 @@ -{% extends "airflow/master.html" %} -{% block head_css %} -{{ super() }} -<link rel="stylesheet" type="text/css" - href="{{ url_for("static", filename="main.css") }}"> -<link rel="stylesheet" type="text/css" - href="{{ url_for("static", filename="dataTables.bootstrap.css") }}"> -<style> - pre { - margin: 0px; - padding: 0px; - border: none; - background: none; - background-color: #FFF; - } - .highcharts-tooltip>span { - background: rgba(255,255,255,0.85); - border: 1px solid silver; - border-radius: 3px; - box-shadow: 1px 1px 2px #888; - padding: 8px; - z-index: 2; - } -.panel-heading .accordion-toggle:after { - /* symbol for "opening" panels */ - font-family: 'Glyphicons Halflings'; /* essential for enabling glyphicon */ - content: "\e114"; /* adjust as needed, taken from bootstrap.css */ - float: right; /* adjust as needed */ - color: grey; /* adjust as needed */ -} -.panel-heading .accordion-toggle.collapsed:after { - /* symbol for "collapsed" panels */ - content: "\e080"; /* adjust as needed, taken from bootstrap.css */ -} -{% if embed %} -/* override padding formatting for flush margins */ -.container { - padding: 0px; -} -body { - padding: 0px; - background-color: #fff; -} -.navbar { - display: none; -} -{% endif %} -</style> -{% endblock %} -{% block title %} -{{ title }} -{% endblock %} -{% block body %} -{{ super() }} -{% if embed %} - <div id="chart_body"> - <img src="{{ url_for('static', filename='loading.gif') }}" width="50px"> - </div> -{% else %} - <div id="container"> - <h2> - <span id="label">{{ label }}</span> - <a href="/admin/chart/edit/?id={{ chart.id }}" > - <span class="glyphicon glyphicon-edit" aria-hidden="true" ></span> - </a> - </h2> - <div id="error" style="display: none;" class="alert alert-danger" role="alert"> - <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> - <span id="error_msg">Oops.</span> - </div> - {% if chart.show_sql %} - <div class="panel panel-default"> - <div class="panel-heading" role="tab" id="headingTwo"> - <h4 class="panel-title"> - <a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion" href="#sql_panel" aria-expanded="true" aria-controls="sql_panel"> - SQL - </a> - </h4> - </div> - <div id="sql_panel" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingTwo"> - <div class="panel-body" id="sql_panel_body"> - {{ sql }} - </div> - </div> - </div> - {% endif %} - {% if chart.chart_type != "datatable" %} - <div id="chart_section" class="panel panel-default"> - <div class="panel-heading" role="tab" id="headingTwo"> - <h4 class="panel-title"> - <a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion" href="#chart_panel" aria-expanded="true" aria-controls="chart_panel"> - Chart - </a> - </h4> - </div> - <div id="chart_panel" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingTwo"> - <div class="panel-body"> - <div id="chart_body"> - <img src="{{ url_for('static', filename='loading.gif') }}" width="50px"> - </div> - </div> - </div> - </div> - {% endif %} - {% if chart.show_datatable or chart.chart_type == "datatable" %} - <div id="datatable_section" class="panel panel-default"> - <div class="panel-heading" role="tab" id="headingTwo"> - <h4 class="panel-title"> - <a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion" href="#datatable_panel" aria-expanded="true" aria-controls="datatable_panel"> - Data - </a> - </h4> - </div> - <div id="datatable_panel" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingTwo"> - <div class="panel-body" id="datatable_panel_body"> - <table id="datatable" class="dataframe table table-bordered table-striped no-wrap"></table> - <img id="loading" src="{{ url_for('static', filename='loading.gif') }}" width="50px"> - </div> - </div> - </div> - {% endif %} - </div> -{% endif %} -{% endblock %} -{% block tail %} - {{ super() }} - <script src="{{ url_for('static', filename='jquery.dataTables.min.js') }}"></script> - <script src="{{ url_for('static', filename='highcharts.js') }}"></script> - <script src="{{ url_for('static', filename='highcharts-more.js') }}"> - </script> - <script src="{{ url_for('static', filename='heatmap.js') }}"></script> - <script src="{{ url_for('static', filename='heatmap-canvas.js') }}"></script> - <script> - function error(msg){ - $('#error_msg').html(msg); - $('#error').show(); - $('#loading').hide(); - $('#chart_section').hide(1000); - $('#datatable_section').hide(1000); - } - function warn(msg){ - $('#error_msg').html(msg); - $('#error').show(); - } - $( document ).ready(function() { - Highcharts.setOptions({ - colors: [ - "#FF5A5F", "#007A87", "#7B0051", "#00D1C1", "#8CE071", "#FFB400", - "#FFAA91", "#B4A76C", "#9CA299", "#565A5C" - ], - }); - url = "{{ url_for('airflow.chart_data') }}" + location.search; - $.getJSON(url, function(payload) { - $('#loading').hide(); - $("#sql_panel_body").html(payload.sql_html); - $("#label").html(payload.label); - if (payload.state == "SUCCESS") { - {% if chart.chart_type != "datatable" %} - $('#chart_body').css('width', '100%'); - $('#chart_body').css('height', '{{ chart.height }}'); - $('#chart_body').highcharts(payload.hc); - {% endif %} - {% if chart.show_datatable or chart.chart_type == "datatable" %} - $('#datatable').dataTable( { - "data": payload.data.data, - "columns": payload.data.columns, - "scrollX": true, - "iDisplayLength": 100, - }); - {% endif %} - } - else { - error(payload.error); - } - if ('warning' in payload) - warn(payload.warning); - }).fail(function(jqxhr, textStatus, err) { - error( textStatus + ': ' + err ); - }); - }); - </script> - -{% endblock %} http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/0a460081/airflow/www/templates/airflow/nvd3.html ---------------------------------------------------------------------- diff --git a/airflow/www/templates/airflow/nvd3.html b/airflow/www/templates/airflow/nvd3.html new file mode 100644 index 0000000..49c35ea --- /dev/null +++ b/airflow/www/templates/airflow/nvd3.html @@ -0,0 +1,175 @@ +{% extends "airflow/master.html" %} +{% block head_css %} +{{ super() }} +<link rel="stylesheet" type="text/css" + href="{{ url_for("static", filename="main.css") }}"> +<link rel="stylesheet" type="text/css" + href="{{ url_for("static", filename="dataTables.bootstrap.css") }}"> +<link rel="stylesheet" type="text/css" + href="{{ url_for("static", filename="nv.d3.css") }}"> +<style> + pre { + margin: 0px; + padding: 0px; + border: none; + background: none; + background-color: #FFF; + } +.panel-heading .accordion-toggle:after { + /* symbol for "opening" panels */ + font-family: 'Glyphicons Halflings'; /* essential for enabling glyphicon */ + content: "\e114"; /* adjust as needed, taken from bootstrap.css */ + float: right; /* adjust as needed */ + color: grey; /* adjust as needed */ +} +.panel-heading .accordion-toggle.collapsed:after { + /* symbol for "collapsed" panels */ + content: "\e080"; /* adjust as needed, taken from bootstrap.css */ +} +{% if embed %} +/* override padding formatting for flush margins */ +.container { + padding: 0px; +} +body { + padding: 0px; + background-color: #fff; +} +.navbar { + display: none; +} +{% endif %} +</style> +{% endblock %} +{% block title %} +{{ title }} +{% endblock %} +{% block body %} +{{ super() }} +{% if embed %} + <div id="chart_body"> + <img src="{{ url_for('static', filename='loading.gif') }}" width="50px"> + </div> +{% else %} + <div id="container"> + <h2> + <span id="label">{{ label }}</span> + <a href="/admin/chart/edit/?id={{ chart.id }}" > + <span class="glyphicon glyphicon-edit" aria-hidden="true" ></span> + </a> + </h2> + <div id="error" style="display: none;" class="alert alert-danger" role="alert"> + <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> + <span id="error_msg">Oops.</span> + </div> + {% if chart.show_sql %} + <div class="panel panel-default"> + <div class="panel-heading" role="tab" id="headingTwo"> + <h4 class="panel-title"> + <a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion" href="#sql_panel" aria-expanded="true" aria-controls="sql_panel"> + SQL + </a> + </h4> + </div> + <div id="sql_panel" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingTwo"> + <div class="panel-body" id="sql_panel_body"> + {{ sql }} + </div> + </div> + </div> + {% endif %} + {% if chart.chart_type != "datatable" %} + <div id="chart_section" class="panel panel-default"> + <div class="panel-heading" role="tab" id="headingTwo"> + <h4 class="panel-title"> + <a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion" href="#chart_panel" aria-expanded="true" aria-controls="chart_panel"> + Chart + </a> + </h4> + </div> + <div id="chart_panel" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingTwo"> + <div class="panel-body"> + <div id="chart_body"> + <img src="{{ url_for('static', filename='loading.gif') }}" width="50px"> + </div> + </div> + </div> + </div> + {% endif %} + {% if chart.show_datatable or chart.chart_type == "datatable" %} + <div id="datatable_section" class="panel panel-default"> + <div class="panel-heading" role="tab" id="headingTwo"> + <h4 class="panel-title"> + <a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion" href="#datatable_panel" aria-expanded="true" aria-controls="datatable_panel"> + Data + </a> + </h4> + </div> + <div id="datatable_panel" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingTwo"> + <div class="panel-body" id="datatable_panel_body"> + <table id="datatable" class="dataframe table table-bordered table-striped no-wrap"></table> + <img id="loading" src="{{ url_for('static', filename='loading.gif') }}" width="50px"> + </div> + </div> + </div> + {% endif %} + </div> +{% endif %} +{% endblock %} +{% block tail %} + {{ super() }} + <script src="{{ url_for('static', filename='jquery.dataTables.min.js') }}"></script> + <script src="{{ url_for('static', filename='d3.v3.min.js') }}"></script> + <script src="{{ url_for('static', filename='nv.d3.js') }}"></script> + <script> + function error(msg){ + $('#error_msg').html(msg); + $('#error').show(); + $('#loading').hide(); + $('#chart_section').hide(1000); + $('#datatable_section').hide(1000); + } + function warn(msg){ + $('#error_msg').html(msg); + $('#error').show(); + } + $( document ).ready(function() { + colors: [ + "#FF5A5F", "#007A87", "#7B0051", "#00D1C1", "#8CE071", "#FFB400", + "#FFAA91", "#B4A76C", "#9CA299", "#565A5C" + ]; + url = "{{ url_for('airflow.chart_data') }}" + location.search; + $.getJSON(url, function(payload) { + $('#loading').hide(); + if (payload.error !== undefined) { + $('#chart_body').html('<div class="alert alert-danger">' + payload.error + '</div>'); + } + $("#sql_panel_body").html(payload.sql_html); + $("#label").html(payload.label); + if (payload.state == "SUCCESS") { + {% if chart.chart_type != "datatable" %} + $('#chart_body').css('width', '100%'); + console.log(payload); + $('#chart_body').html(payload.htmlcontent); + {% endif %} + {% if chart.show_datatable or chart.chart_type == "datatable" %} + $('#datatable').dataTable( { + "data": payload.data.data, + "columns": payload.data.columns, + "scrollX": true, + "iDisplayLength": 100, + }); + {% endif %} + } + else { + error(payload.error); + } + if ('warning' in payload) + warn(payload.warning); + }).fail(function(jqxhr, textStatus, err) { + error( textStatus + ': ' + err ); + }); + }); + </script> + +{% endblock %} http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/0a460081/airflow/www/utils.py ---------------------------------------------------------------------- diff --git a/airflow/www/utils.py b/airflow/www/utils.py index d600cbb..7288a10 100644 --- a/airflow/www/utils.py +++ b/airflow/www/utils.py @@ -16,21 +16,23 @@ from future import standard_library standard_library.install_aliases() from builtins import str from builtins import object + from cgi import escape from io import BytesIO as IO import functools import gzip import dateutil.parser as dateparser import json +import time + from flask import after_this_request, request, Response from flask_login import current_user -from jinja2 import Template import wtforms from wtforms.compat import text_type from airflow import configuration, models, settings from airflow.utils.json import AirflowJsonEncoder -from airflow.utils.email import send_email + AUTHENTICATE = configuration.getboolean('webserver', 'AUTHENTICATE') @@ -87,6 +89,11 @@ def limit_sql(sql, limit, conn_type): return sql +def epoch(dttm): + """Returns an epoch-type date""" + return int(time.mktime(dttm.timetuple())) * 1000, + + def action_logging(f): ''' Decorator to log user actions http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/0a460081/airflow/www/views.py ---------------------------------------------------------------------- diff --git a/airflow/www/views.py b/airflow/www/views.py index 6570adc..f26ca7b 100644 --- a/airflow/www/views.py +++ b/airflow/www/views.py @@ -12,31 +12,29 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import sys import os import pkg_resources import socket import importlib - from functools import wraps from datetime import datetime, timedelta import dateutil.parser import copy from itertools import chain, product +import json from past.utils import old_div from past.builtins import basestring import inspect -import subprocess import traceback import sqlalchemy as sqla from sqlalchemy import or_, desc, and_ - -from flask import redirect, url_for, request, Markup, Response, current_app, render_template +from flask import ( + redirect, url_for, request, Markup, Response, current_app, render_template) from flask_admin import BaseView, expose, AdminIndexView from flask_admin.contrib.sqla import ModelView from flask_admin.actions import action @@ -45,7 +43,7 @@ from flask._compat import PY2 import jinja2 import markdown -import json +import nvd3 from wtforms import ( Form, SelectField, TextAreaField, PasswordField, StringField) @@ -291,6 +289,8 @@ class Airflow(BaseView): @wwwutils.gzipped # @cache.cached(timeout=3600, key_prefix=wwwutils.make_cache_key) def chart_data(self): + from airflow import macros + import pandas as pd session = settings.Session() chart_id = request.args.get('chart_id') csv = request.args.get('csv') == "true" @@ -301,6 +301,7 @@ class Airflow(BaseView): session.commit() session.close() + payload = {} payload['state'] = 'ERROR' payload['error'] = '' @@ -317,7 +318,6 @@ class Airflow(BaseView): "a Python dictionary. ") request_dict = {k: request.args.get(k) for k in request.args} - from airflow import macros args.update(request_dict) args['macros'] = macros sql = jinja2.Template(chart.sql).render(**args) @@ -329,11 +329,11 @@ class Airflow(BaseView): ) payload['label'] = label - import pandas as pd pd.set_option('display.max_colwidth', 100) hook = db.get_hook() try: - df = hook.get_pandas_df(wwwutils.limit_sql(sql, CHART_LIMIT, conn_type=db.conn_type)) + df = hook.get_pandas_df( + wwwutils.limit_sql(sql, CHART_LIMIT, conn_type=db.conn_type)) df = df.fillna(0) except Exception as e: payload['error'] += "SQL execution failed. Details: " + str(e) @@ -352,26 +352,25 @@ class Airflow(BaseView): if not payload['error'] and len(df) == 0: payload['error'] += "Empty result set. " elif ( - not payload['error'] and - chart.sql_layout == 'series' and - chart.chart_type != "datatable" and - len(df.columns) < 3): + not payload['error'] and + chart.sql_layout == 'series' and + chart.chart_type != "datatable" and + len(df.columns) < 3): payload['error'] += "SQL needs to return at least 3 columns. " elif ( - not payload['error'] and - chart.sql_layout == 'columns'and - len(df.columns) < 2): + not payload['error'] and + chart.sql_layout == 'columns'and + len(df.columns) < 2): payload['error'] += "SQL needs to return at least 2 columns. " elif not payload['error']: import numpy as np chart_type = chart.chart_type data = None - if chart_type == "datatable": - chart.show_datatable = True - if chart.show_datatable: + if chart.show_datatable or chart_type == "datatable": data = df.to_dict(orient="split") data['columns'] = [{'title': c} for c in data['columns']] + payload['data'] = data # Trying to convert time to something Highcharts likes x_col = 1 if chart.sql_layout == 'series' else 0 @@ -380,101 +379,14 @@ class Airflow(BaseView): # From string to datetime df[df.columns[x_col]] = pd.to_datetime( df[df.columns[x_col]]) + df[df.columns[x_col]] = df[df.columns[x_col]].apply( + lambda x: int(x.strftime("%s")) * 1000) except Exception as e: - raise AirflowException(str(e)) - df[df.columns[x_col]] = df[df.columns[x_col]].apply( - lambda x: int(x.strftime("%s")) * 1000) + payload['error'] = "Time conversion failed" - series = [] - colorAxis = None if chart_type == 'datatable': - payload['data'] = data payload['state'] = 'SUCCESS' return wwwutils.json_response(payload) - - elif chart_type == 'para': - df.rename(columns={ - df.columns[0]: 'name', - df.columns[1]: 'group', - }, inplace=True) - return Response( - response=df.to_csv(index=False), - status=200, - mimetype="application/text") - - elif chart_type == 'heatmap': - color_perc_lbound = float( - request.args.get('color_perc_lbound', 0)) - color_perc_rbound = float( - request.args.get('color_perc_rbound', 1)) - color_scheme = request.args.get('color_scheme', 'blue_red') - - if color_scheme == 'blue_red': - stops = [ - [color_perc_lbound, '#00D1C1'], - [ - color_perc_lbound + - ((color_perc_rbound - color_perc_lbound)/2), - '#FFFFCC' - ], - [color_perc_rbound, '#FF5A5F'] - ] - elif color_scheme == 'blue_scale': - stops = [ - [color_perc_lbound, '#FFFFFF'], - [color_perc_rbound, '#2222FF'] - ] - elif color_scheme == 'fire': - diff = float(color_perc_rbound - color_perc_lbound) - stops = [ - [color_perc_lbound, '#FFFFFF'], - [color_perc_lbound + 0.33*diff, '#FFFF00'], - [color_perc_lbound + 0.66*diff, '#FF0000'], - [color_perc_rbound, '#000000'] - ] - else: - stops = [ - [color_perc_lbound, '#FFFFFF'], - [ - color_perc_lbound + - ((color_perc_rbound - color_perc_lbound)/2), - '#888888' - ], - [color_perc_rbound, '#000000'], - ] - - xaxis_label = df.columns[1] - yaxis_label = df.columns[2] - data = [] - for row in df.itertuples(): - data.append({ - 'x': row[2], - 'y': row[3], - 'value': row[4], - }) - x_format = '{point.x:%Y-%m-%d}' \ - if chart.x_is_date else '{point.x}' - series.append({ - 'data': data, - 'borderWidth': 0, - 'colsize': 24 * 36e5, - 'turboThreshold': sys.float_info.max, - 'tooltip': { - 'headerFormat': '', - 'pointFormat': ( - df.columns[1] + ': ' + x_format + '<br/>' + - df.columns[2] + ': {point.y}<br/>' + - df.columns[3] + ': <b>{point.value}</b>' - ), - }, - }) - colorAxis = { - 'stops': stops, - 'minColor': '#FFFFFF', - 'maxColor': '#000000', - 'min': 50, - 'max': 2200, - } else: if chart.sql_layout == 'series': # User provides columns (series, x, y) @@ -495,63 +407,22 @@ class Airflow(BaseView): for col in df.columns: df[col] = df[col].astype(np.float) + df = df.fillna(0) + NVd3ChartClass = chart_mapping.get(chart.chart_type) + NVd3ChartClass = getattr(nvd3, NVd3ChartClass) + nvd3_chart = NVd3ChartClass(x_is_date=chart.x_is_date) + for col in df.columns: - series.append({ - 'name': col, - 'data': [ - (k, df[col][k]) - for k in df[col].keys() - if not np.isnan(df[col][k])] - }) - series = [serie for serie in sorted( - series, key=lambda s: s['data'][0][1], reverse=True)] - - if chart_type == "stacked_area": - stacking = "normal" - chart_type = 'area' - elif chart_type == "percent_area": - stacking = "percent" - chart_type = 'area' - else: - stacking = None - hc = { - 'chart': { - 'type': chart_type - }, - 'plotOptions': { - 'series': { - 'marker': { - 'enabled': False - } - }, - 'area': {'stacking': stacking}, - }, - 'title': {'text': ''}, - 'xAxis': { - 'title': {'text': xaxis_label}, - 'type': 'datetime' if chart.x_is_date else None, - }, - 'yAxis': { - 'title': {'text': yaxis_label}, - }, - 'colorAxis': colorAxis, - 'tooltip': { - 'useHTML': True, - 'backgroundColor': None, - 'borderWidth': 0, - }, - 'series': series, - } + nvd3_chart.add_serie(name=col, y=df[col].tolist(), x=df[col].index.tolist()) + try: + nvd3_chart.buildcontent() + payload['chart_type'] = nvd3_chart.__class__.__name__ + payload['htmlcontent'] = nvd3_chart.htmlcontent + except Exception as e: + payload['error'] = str(e) - if chart.y_log_scale: - hc['yAxis']['type'] = 'logarithmic' - hc['yAxis']['minorTickInterval'] = 0.1 - if 'min' in hc['yAxis']: - del hc['yAxis']['min'] payload['state'] = 'SUCCESS' - payload['hc'] = hc - payload['data'] = data payload['request_dict'] = request_dict return wwwutils.json_response(payload) @@ -565,8 +436,14 @@ class Airflow(BaseView): session.expunge_all() session.commit() session.close() - if chart.chart_type == 'para': - return self.render('airflow/para/para.html', chart=chart) + + NVd3ChartClass = chart_mapping.get(chart.chart_type) + if not NVd3ChartClass: + flash( + "Not supported anymore as the license was incompatible, " + "sorry", + "danger") + redirect('/admin/chart/') sql = "" if chart.show_sql: @@ -576,7 +453,7 @@ class Airflow(BaseView): HtmlFormatter(noclasses=True)) ) return self.render( - 'airflow/highchart.html', + 'airflow/nvd3.html', chart=chart, title="Airflow - Chart", sql=sql, @@ -1476,38 +1353,27 @@ class Airflow(BaseView): include_upstream=True, include_downstream=False) - all_data = [] max_duration = 0 + chart = nvd3.lineChart( + name="lineChart", x_is_date=True, height=600, width="1200") + for task in dag.tasks: - data = [] + y = [] + x = [] for ti in task.get_task_instances(session, start_date=min_date, end_date=base_date): if ti.duration: - data.append([ - ti.execution_date.isoformat(), - ti.duration - ]) if max_duration < ti.duration: max_duration = ti.duration - if data: - all_data.append({'data': data, 'name': task.task_id}) - - def divide_durations(all_data, denom): - for data in all_data: - for d in data['data']: - d[1] /= denom - - if 60*60 < max_duration: - unit = 'hours' - divide_durations(all_data, float(60*60)) - elif 60 < max_duration: - unit = 'minutes' - divide_durations(all_data, 60.0) - else: - unit = 'seconds' + + dttm = wwwutils.epoch(ti.execution_date) + x.append(dttm) + y.append(float(ti.duration) / (60*60)) + if x: + chart.add_serie(name=task.task_id, x=x, y=y) tis = dag.get_task_instances( - session, start_date=min_date, end_date=base_date) + session, start_date=min_date, end_date=base_date) dates = sorted(list({ti.execution_date for ti in tis})) max_date = max([ti.execution_date for ti in tis]) if dates else None @@ -1516,15 +1382,14 @@ class Airflow(BaseView): form = DateTimeWithNumRunsForm(data={'base_date': max_date, 'num_runs': num_runs}) + chart.buildhtml() return self.render( 'airflow/chart.html', dag=dag, - data=json.dumps(all_data), - chart_options={'yAxis': {'title': {'text': unit}}}, - height="700px", demo_mode=conf.getboolean('webserver', 'demo_mode'), root=root, form=form, + chart=chart, ) @expose('/landing_times') @@ -1553,18 +1418,23 @@ class Airflow(BaseView): include_upstream=True, include_downstream=False) - all_data = [] + chart = nvd3.lineChart( + name="lineChart", x_is_date=True, height=600, width="1200") for task in dag.tasks: - data = [] + y = [] + x = [] for ti in task.get_task_instances(session, start_date=min_date, end_date=base_date): + ts = ti.execution_date + if dag.schedule_interval: + ts = dag.following_schedule(ts) if ti.end_date: - ts = ti.execution_date - if dag.schedule_interval: - ts = dag.following_schedule(ts) + dttm = wwwutils.epoch(ti.execution_date) secs = old_div((ti.end_date - ts).total_seconds(), 60*60) - data.append([ti.execution_date.isoformat(), secs]) - all_data.append({'data': data, 'name': task.task_id}) + x.append(dttm) + y.append(secs) + if x: + chart.add_serie(name=task.task_id, x=x, y=y) tis = dag.get_task_instances( session, start_date=min_date, end_date=base_date) @@ -1579,9 +1449,8 @@ class Airflow(BaseView): return self.render( 'airflow/chart.html', dag=dag, - data=json.dumps(all_data), + chart=chart, height="700px", - chart_options={'yAxis': {'title': {'text': 'hours after 00:00'}}}, demo_mode=conf.getboolean('webserver', 'demo_mode'), root=root, form=form, @@ -1639,7 +1508,6 @@ class Airflow(BaseView): @login_required @wwwutils.action_logging def gantt(self): - session = settings.Session() dag_id = request.args.get('dag_id') dag = dagbag.get_dag(dag_id) @@ -1661,57 +1529,40 @@ class Airflow(BaseView): form = DateTimeForm(data={'execution_date': dttm}) tis = [ - ti - for ti in dag.get_task_instances(session, dttm, dttm) + ti for ti in dag.get_task_instances(session, dttm, dttm) if ti.start_date] tis = sorted(tis, key=lambda ti: ti.start_date) + tasks = [] - data = [] - for i, ti in enumerate(tis): - end_date = ti.end_date or datetime.now() - tasks += [ti.task_id] - color = State.color(ti.state) - data.append({ - 'x': i, - 'low': int(ti.start_date.strftime('%s')) * 1000, - 'high': int(end_date.strftime('%s')) * 1000, - 'color': color, + for ti in tis: + tasks.append({ + 'startDate': wwwutils.epoch(ti.start_date), + 'endDate': wwwutils.epoch(ti.end_date or datetime.now()), + 'isoStart': ti.start_date.isoformat()[:-4], + 'isoEnd': ti.end_date.isoformat()[:-4], + 'taskName': ti.task_id, + 'duration': "{}".format(ti.end_date - ti.start_date)[:-4], + 'status': ti.state, + 'executionDate': ti.execution_date.isoformat(), }) - height = (len(tis) * 25) + 50 + states = {ti.state:ti.state for ti in tis} + data = { + 'taskNames': [ti.task_id for ti in tis], + 'tasks': tasks, + 'taskStatus': states, + 'height': len(tis) * 25, + } + session.commit() session.close() - hc = { - 'chart': { - 'type': 'columnrange', - 'inverted': True, - 'height': height, - }, - 'xAxis': {'categories': tasks, 'alternateGridColor': '#FAFAFA'}, - 'yAxis': {'type': 'datetime'}, - 'title': { - 'text': None - }, - 'plotOptions': { - 'series': { - 'cursor': 'pointer', - 'minPointLength': 4, - }, - }, - 'legend': { - 'enabled': False - }, - 'series': [{ - 'data': data - }] - } return self.render( 'airflow/gantt.html', dag=dag, execution_date=dttm.isoformat(), form=form, - hc=json.dumps(hc, indent=4), - height=height, + data=json.dumps(data, indent=2), + base_date='', demo_mode=demo_mode, root=root, ) @@ -1984,12 +1835,10 @@ class ChartModelView(wwwutils.DataProfilingMixin, AirflowModelView): ('line', 'Line Chart'), ('spline', 'Spline Chart'), ('bar', 'Bar Chart'), - ('para', 'Parallel Coordinates'), ('column', 'Column Chart'), ('area', 'Overlapping Area Chart'), ('stacked_area', 'Stacked Area Chart'), ('percent_area', 'Percent Area Chart'), - ('heatmap', 'Heatmap'), ('datatable', 'No chart, data table only'), ], 'sql_layout': [ @@ -2014,6 +1863,18 @@ class ChartModelView(wwwutils.DataProfilingMixin, AirflowModelView): model.user_id = current_user.id model.last_modified = datetime.now() +chart_mapping = ( + ('line', 'lineChart'), + ('spline', 'lineChart'), + ('bar', 'multiBarChart'), + ('column', 'multiBarChart'), + ('area', 'stackedAreaChart'), + ('stacked_area', 'stackedAreaChart'), + ('percent_area', 'stackedAreaChart'), + ('datatable', 'datatable'), +) +chart_mapping = dict(chart_mapping) + class KnowEventView(wwwutils.DataProfilingMixin, AirflowModelView): verbose_name = "known event" @@ -2314,7 +2175,12 @@ class ConnectionModelView(wwwutils.SuperUserMixin, AirflowModelView): @classmethod def alert_fernet_key(cls): - return conf.get('core', 'fernet_key') is None + fk = None + try: + fk = conf.get('core', 'fernet_key') + except: + pass + return fk is None @classmethod def is_secure(self): http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/0a460081/setup.py ---------------------------------------------------------------------- diff --git a/setup.py b/setup.py index 411a699..978c484 100644 --- a/setup.py +++ b/setup.py @@ -155,13 +155,13 @@ github_enterprise = ['Flask-OAuthlib>=0.9.1'] qds = ['qds-sdk>=1.9.0'] cloudant = ['cloudant>=0.5.9,<2.0'] # major update coming soon, clamp to 0.x - all_dbs = postgres + mysql + hive + mssql + hdfs + vertica + cloudant devel = ['lxml>=3.3.4', 'nose', 'nose-parameterized', 'mock', 'click', 'jira'] devel_minreq = devel + mysql + doc + password + s3 devel_hadoop = devel_minreq + hive + hdfs + webhdfs + kerberos devel_all = devel + all_dbs + doc + samba + s3 + slack + crypto + oracle + docker + def do_setup(): write_version() setup( @@ -177,34 +177,35 @@ def do_setup(): install_requires=[ 'alembic>=0.8.3, <0.9', 'babel>=1.3, <2.0', - 'chartkick>=0.4.2, < 0.5', 'croniter>=0.3.8, <0.4', 'dill>=0.2.2, <0.3', - 'python-daemon>=2.1.1, <2.2', 'flask>=0.10.1, <0.11', 'flask-admin==1.4.1', 'flask-cache>=0.13.1, <0.14', 'flask-login==0.2.11', - 'future>=0.15.0, <0.16', + 'flask-wtf==0.12', 'funcsigs>=0.4, <1', + 'future>=0.15.0, <0.16', 'gitpython>=2.0.2', 'gunicorn>=19.3.0, <19.4.0', # 19.4.? seemed to have issues 'jinja2>=2.7.3, <3.0', 'markdown>=2.5.2, <3.0', 'pandas>=0.15.2, <1.0.0', 'pygments>=2.0.1, <3.0', + 'python-daemon>=2.1.1, <2.2', 'python-dateutil>=2.3, <3', + 'python-nvd3==0.14.2', 'requests>=2.5.1, <3', 'setproctitle>=1.1.8, <2', 'sqlalchemy>=0.9.8', 'thrift>=0.9.2, <0.10', - 'Flask-WTF==0.12' ], extras_require={ 'all': devel_all, 'all_dbs': all_dbs, 'async': async, 'celery': celery, + 'cloudant': cloudant, 'crypto': crypto, 'devel': devel_minreq, 'devel_hadoop': devel_hadoop, @@ -212,26 +213,25 @@ def do_setup(): 'docker': docker, 'druid': druid, 'gcp_api': gcp_api, + 'github_enterprise': github_enterprise, 'hdfs': hdfs, 'hive': hive, 'jdbc': jdbc, + 'kerberos': kerberos, + 'ldap': ldap, 'mssql': mssql, 'mysql': mysql, 'oracle': oracle, + 'password': password, 'postgres': postgres, + 'qds': qds, 'rabbitmq': rabbitmq, 's3': s3, 'samba': samba, 'slack': slack, 'statsd': statsd, 'vertica': vertica, - 'ldap': ldap, 'webhdfs': webhdfs, - 'kerberos': kerberos, - 'password': password, - 'github_enterprise': github_enterprise, - 'qds': qds, - 'cloudant': cloudant }, classifiers=[ 'Development Status :: 5 - Production/Stable', @@ -257,3 +257,4 @@ def do_setup(): if __name__ == "__main__": do_setup() + http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/0a460081/tests/core.py ---------------------------------------------------------------------- diff --git a/tests/core.py b/tests/core.py index 5e6a4fd..8dfd81a 100644 --- a/tests/core.py +++ b/tests/core.py @@ -785,32 +785,13 @@ class WebUiTests(unittest.TestCase): response = self.app.get( '/admin/airflow/tree?num_runs=25&dag_id=example_bash_operator') assert "runme_0" in response.data.decode('utf-8') - # new Chartkick.LineChart(document.getElementById("chart-0"), [{"data": [["2015-11-17T16:53:08.652950", 9.866944444444444e-06]], "name": "run_after_loop"}, {"data": [["2015-11-17T16:53:08.652950", 0.0002858047222222222], ["2015-11-17T16:56:09.698921", 0.00028737944444444445]], "name": "runme_0"}, {"data": [["2015-11-17T16:53:08.652950", 0.0002863941666666666], ["2015-11-17T16:56:09.698921", 0.00029015249999999996]], "name": "runme_1"}, {"data": [["2015-11-17T16:53:08.652950", 0.0002860847222222222], ["2015-11-17T16:56:09.698921", 0.00029001583333333335]], "name": "runme_2"}, {"data": [["2015-11-17T16:53:08.652950", 8.166944444444444e-06], ["2015-11-17T16:56:09.698921", 1.2806944444444445e-05]], "name": "also_run_this"}], {"library": {"yAxis": {"title": {"text": "hours"}}}, "height": "700px"}); - - chartkick_regexp = 'new Chartkick.LineChart\(document.getElementById\("chart-\d+"\),(.+)\)\;' response = self.app.get( '/admin/airflow/duration?days=30&dag_id=example_bash_operator') assert "example_bash_operator" in response.data.decode('utf-8') - - chartkick_matched = re.search(chartkick_regexp, - response.data.decode('utf-8')) - assert chartkick_matched is not None, "chartkick_matched was none. Expected regex is: %s\nResponse was: %s" % ( - chartkick_regexp, - response.data.decode('utf-8')) - - # test that parameters to LineChart are well-formed json - try: - json.loads('[%s]' % chartkick_matched.group(1)) - except e: - assert False, "Exception while json parsing LineChart parameters: %s" % e - response = self.app.get( '/admin/airflow/landing_times?' 'days=30&dag_id=example_bash_operator') assert "example_bash_operator" in response.data.decode('utf-8') - chartkick_matched = re.search(chartkick_regexp, - response.data.decode('utf-8')) - assert chartkick_matched is not None response = self.app.get( '/admin/airflow/gantt?dag_id=example_bash_operator') assert "example_bash_operator" in response.data.decode('utf-8')