diff --git a/TODO.md b/TODO.md index 60c7788f683b5..40c646ac8671f 100644 --- a/TODO.md +++ b/TODO.md @@ -6,3 +6,5 @@ * Save / bookmark / url shortener * SQL: Find a way to manage granularity * Create ~/.panoramix/ to host DB and config, generate default config there +* Add a per-datasource permission + diff --git a/panoramix/forms.py b/panoramix/forms.py new file mode 100644 index 0000000000000..82b50c28b50ff --- /dev/null +++ b/panoramix/forms.py @@ -0,0 +1,90 @@ +from wtforms import Field, Form, SelectMultipleField, SelectField, TextField +from flask_appbuilder.fieldwidgets import Select2Widget, Select2ManyWidget + + + + +class OmgWtForm(Form): + field_order = tuple() + css_classes = dict() + @property + def fields(self): + fields = [] + for field in self.field_order: + if hasattr(self, field): + obj = getattr(self, field) + if isinstance(obj, Field): + fields.append(getattr(self, field)) + return fields + + def get_field(self, fieldname): + return getattr(self, fieldname) + + def field_css_classes(self, fieldname): + if fieldname in self.css_classes: + return " ".join(self.css_classes[fieldname]) + return "" + + +def form_factory(datasource, viz, form_args=None): + from panoramix.viz import viz_types + row_limits = [10, 50, 100, 500, 1000, 5000, 10000] + series_limits = [0, 5, 10, 25, 50, 100, 500] + group_by_choices = [(s, s) for s in datasource.groupby_column_names] + # Pool of all the fields that can be used in Panoramix + px_form_fields = { + 'viz_type': SelectField( + 'Viz', + choices=[(k, v.verbose_name) for k, v in viz_types.items()]), + 'metrics': SelectMultipleField( + 'Metrics', choices=datasource.metrics_combo), + 'groupby': SelectMultipleField( + 'Group by', + choices=[(s, s) for s in datasource.groupby_column_names]), + 'granularity': TextField('Time Granularity', default="one day"), + 'since': TextField('Since', default="one day ago"), + 'until': TextField('Until', default="now"), + 'row_limit': + SelectField( + 'Row limit', choices=[(s, s) for s in row_limits]), + 'limit': + SelectField( + 'Series limit', choices=[(s, s) for s in series_limits]), + 'rolling_type': SelectField( + 'Rolling', + choices=[(s, s) for s in ['mean', 'sum', 'std']]), + 'rolling_periods': TextField('Periods',), + 'series': SelectField('Series', choices=group_by_choices), + 'entity': SelectField('Entity', choices=group_by_choices), + 'x': SelectField('X Axis', choices=datasource.metrics_combo), + 'y': SelectField('Y Axis', choices=datasource.metrics_combo), + 'size': SelectField('Bubble Size', choices=datasource.metrics_combo), + } + field_css_classes = {k: ['form-control'] for k in px_form_fields.keys()} + select2 = [ + 'viz_type', 'metrics', 'groupby', + 'row_limit', 'rolling_type', 'series', + 'entity', 'x', 'y', 'size',] + field_css_classes['since'] += ['select2_free_since'] + field_css_classes['until'] += ['select2_free_until'] + field_css_classes['granularity'] += ['select2_free_granularity'] + for field in select2: + field_css_classes[field] += ['select2'] + + + class QueryForm(OmgWtForm): + field_order = viz.form_fields + css_classes = field_css_classes + + for i in range(10): + setattr(QueryForm, 'flt_col_' + str(i), SelectField( + 'Filter 1', choices=[(s, s) for s in datasource.filterable_column_names])) + setattr(QueryForm, 'flt_op_' + str(i), SelectField( + 'Filter 1', choices=[(m, m) for m in ['in', 'not in']])) + setattr(QueryForm, 'flt_eq_' + str(i), TextField("Super")) + for ff in viz.form_fields: + if isinstance(ff, basestring): + ff = [ff] + for s in ff: + setattr(QueryForm, s, px_form_fields[s]) + return QueryForm diff --git a/panoramix/templates/panoramix/datasource.html b/panoramix/templates/panoramix/datasource.html index 6ab628e2570f1..2a23e41abd572 100644 --- a/panoramix/templates/panoramix/datasource.html +++ b/panoramix/templates/panoramix/datasource.html @@ -38,32 +38,27 @@ <h3> <hr> <form id="query" method="GET" style="display: none;"> - <div>{{ form.viz_type.label }}: {{ form.viz_type(class_="form-control select2") }}</div> - {% if 'metrics' not in viz.hidden_fields %} - <div>{{ form.metrics.label }}: {{ form.metrics(class_="form-control select2") }}</div> - {% endif %} - {% if 'granularity' not in viz.hidden_fields %} - <div>{{ form.granularity.label }} - <i class="fa fa-info-circle" data-toggle="tooltip" data-placement="right" - title="Supports natural language time as in '10 seconds', '1 day' or '1 week'" - id="blah"></i> - {{ form.granularity(class_="form-control select2_free_granularity") }}</div> - {% endif %} - <div class="row"> - <div class="form-group"> - <div class="col-xs-6">{{ form.since.label }} - <i class="fa fa-info-circle" data-toggle="tooltip" data-placement="right" - title="Supports natural language time as in '1 day ago', '28 days' or '3 years'" - id="blah"></i> - {{ form.since(class_="form-control select2_free_since") }}</div> - <div class="col-xs-6">{{ form.until.label }} - {{ form.until(class_="form-control select2_free_until") }}</div> - </div> - </div> - {% if 'groupby' not in viz.hidden_fields %} - <div>{{ form.groupby.label }}: {{ form.groupby(class_="form-control select2") }}</div> - {% endif %} - {% block extra_fields %}{% endblock %} + {% for fieldname in form.field_order %} + {% if not fieldname.__iter__ %} + <div> + {% set field = form.get_field(fieldname)%} + {{ field.label }}: + {{ field(class_=form.field_css_classes(field.name)) }} + </div> + {% else %} + <div class="row"> + <div class="form-group"> + {% for name in fieldname %} + <div class="col-xs-{{ (12 / fieldname|length) | int }}"> + {% set field = form.get_field(name)%} + {{ field.label }}: + {{ field(class_=form.field_css_classes(field.name)) }} + </div> + {% endfor %} + </div> + </div> + {% endif %} + {% endfor %} <hr> <h4>Filters</h4> <div id="flt0" style="display: none;"> diff --git a/panoramix/templates/panoramix/viz_highcharts.html b/panoramix/templates/panoramix/viz_highcharts.html index 451ba613c167e..6af92d2cab3f7 100644 --- a/panoramix/templates/panoramix/viz_highcharts.html +++ b/panoramix/templates/panoramix/viz_highcharts.html @@ -4,36 +4,6 @@ <div id="chart"></div> {% endblock %} -{% block extra_fields %} - {% if form.compare %} - <div>{{ form.compare.label }}: {{ form.compare(class_="form-control") }}</div> - {% endif %} - {% if form.rolling_type %} - <div class="row"> - <span class="col col-sm-5">{{ form.rolling_type.label }}: {{ form.rolling_type(class_="form-control select2") }}</span> - <span class="col col-sm-4">{{ form.rolling_periods.label }}: {{ form.rolling_periods(class_="form-control") }}</span> - </div> - {% endif %} - {% if form.limit %} - <div>{{ form.limit.label }}: {{ form.limit(class_="form-control select2") }}</div> - {% endif %} - {% if form.series %} - <div>{{ form.series.label }}: {{ form.series(class_="form-control select2") }}</div> - {% endif %} - {% if form.entity %} - <div>{{ form.entity.label }}: {{ form.entity(class_="form-control select2") }}</div> - {% endif %} - {% if form.size %} - <div>{{ form.size.label }}: {{ form.size(class_="form-control select2") }}</div> - {% endif %} - {% if form.x %} - <div>{{ form.x.label }}: {{ form.x(class_="form-control select2") }}</div> - {% endif %} - {% if form.y %} - <div>{{ form.y.label }}: {{ form.y(class_="form-control select2") }}</div> - {% endif %} -{% endblock %} - {% block tail %} {{ super() }} {% if viz.stockchart %} diff --git a/panoramix/templates/panoramix/viz_table.html b/panoramix/templates/panoramix/viz_table.html index 18fc6e122fba1..4c5b6e09a4396 100644 --- a/panoramix/templates/panoramix/viz_table.html +++ b/panoramix/templates/panoramix/viz_table.html @@ -33,10 +33,6 @@ {% endif %} {% endblock %} -{% block extra_fields %} - <div>{{ form.row_limit.label }}: {{ form.row_limit(class_="form-control select2") }}</div> -{% endblock %} - {% block tail %} {{ super() }} <script src="{{ url_for('static', filename='jquery.dataTables.min.js') }}"></script> diff --git a/panoramix/viz.py b/panoramix/viz.py index 71a7fa03e89d7..0c31b48379fb5 100644 --- a/panoramix/viz.py +++ b/panoramix/viz.py @@ -2,12 +2,13 @@ from flask import flash, request import pandas as pd from collections import OrderedDict -from panoramix import utils -from panoramix.highchart import Highchart, HighchartBubble -from wtforms import Form, SelectMultipleField, SelectField, TextField import config import logging +import numpy as np +from panoramix import utils +from panoramix.highchart import Highchart, HighchartBubble +from panoramix.forms import form_factory CHART_ARGS = { 'height': 700, @@ -16,58 +17,14 @@ } -class OmgWtForm(Form): - field_order = ( - 'viz_type', 'granularity', 'since', 'group_by', 'limit') - def fields(self): - fields = [] - for field in self.field_order: - if hasattr(self, field): - obj = getattr(self, field) - if isinstance(obj, Field): - fields.append(getattr(self, field)) - return fields - - -def form_factory(datasource, form_args=None, extra_fields_dict=None): - extra_fields_dict = extra_fields_dict or {} - - if form_args: - limit = form_args.get("limit") - try: - limit = int(limit) - if limit not in limits: - limits.append(limit) - limits = sorted(limits) - except: - pass - - class QueryForm(OmgWtForm): - viz_type = SelectField( - 'Viz', - choices=[(k, v.verbose_name) for k, v in viz_types.items()]) - metrics = SelectMultipleField('Metrics', choices=datasource.metrics_combo) - groupby = SelectMultipleField( - 'Group by', choices=[ - (s, s) for s in datasource.groupby_column_names]) - granularity = TextField('Time Granularity', default="one day") - since = TextField('Since', default="one day ago") - until = TextField('Until', default="now") - for i in range(10): - setattr(QueryForm, 'flt_col_' + str(i), SelectField( - 'Filter 1', choices=[(s, s) for s in datasource.filterable_column_names])) - setattr(QueryForm, 'flt_op_' + str(i), SelectField( - 'Filter 1', choices=[(m, m) for m in ['in', 'not in']])) - setattr(QueryForm, 'flt_eq_' + str(i), TextField("Super")) - for k, v in extra_fields_dict.items(): - setattr(QueryForm, k, v) - return QueryForm - - class BaseViz(object): verbose_name = "Base Viz" template = "panoramix/datasource.html" hidden_fields = [] + form_fields = [ + 'viz_type', 'metrics', 'groupby', 'granularity', + ('since', 'until')] + def __init__(self, datasource, form_data, view): self.datasource = datasource self.form_class = self.form_class() @@ -90,7 +47,7 @@ def __init__(self, datasource, form_data, view): def form_class(self): - return form_factory(self.datasource, request.args) + return form_factory(self.datasource, self, request.args) def query_filters(self): args = self.form_data @@ -159,6 +116,7 @@ def render(self, *args, **kwargs): class TableViz(BaseViz): verbose_name = "Table View" template = 'panoramix/viz_table.html' + form_fields = BaseViz.form_fields + ['row_limit'] def query_obj(self): d = super(TableViz, self).query_obj() @@ -178,17 +136,11 @@ def render(self): if self.form_data.get("granularity") == "all" and 'timestamp' in df: del df['timestamp'] for m in self.metrics: - import numpy as np df[m + '__perc'] = np.rint((df[m] / np.max(df[m])) * 100) return super(TableViz, self).render(df=df) def form_class(self): - limits = [10, 50, 100, 500, 1000, 5000, 10000] - return form_factory(self.datasource, request.args, - extra_fields_dict={ - 'row_limit': - SelectField('Row limit', choices=[(s, s) for s in limits]) - }) + return form_factory(self.datasource, self, request.args) class HighchartsViz(BaseViz): @@ -204,28 +156,12 @@ class BubbleViz(HighchartsViz): verbose_name = "Bubble Chart" chart_type = 'bubble' hidden_fields = ['granularity', 'metrics', 'groupby'] + form_fields = [ + 'viz_type', 'since', 'until', + 'series', 'entity', 'x', 'y', 'size', 'limit'] def form_class(self): - datasource = self.datasource - limits = [0, 5, 10, 25, 50, 100, 500] - return form_factory(self.datasource, request.args, - extra_fields_dict={ - #'compare': TextField('Period Compare',), - 'series': SelectField( - 'Series', choices=[ - (s, s) for s in datasource.groupby_column_names]), - 'entity': SelectField( - 'Entity', choices=[ - (s, s) for s in datasource.groupby_column_names]), - 'x': SelectField( - 'X Axis', choices=datasource.metrics_combo), - 'y': SelectField( - 'Y Axis', choices=datasource.metrics_combo), - 'size': SelectField( - 'Bubble Size', choices=datasource.metrics_combo), - 'limit': SelectField( - 'Limit', choices=[(s, s) for s in limits]), - }) + return form_factory(self.datasource, self, request.args) def query_obj(self): d = super(BubbleViz, self).query_obj() @@ -264,12 +200,18 @@ def render(self): return super(BubbleViz, self).render(error_msg=self.error_msg) - class TimeSeriesViz(HighchartsViz): verbose_name = "Time Series - Line Chart" chart_type = "spline" stockchart = True sort_legend_y = True + form_fields = [ + 'viz_type', + 'granularity', ('since', 'until'), + 'metrics', + 'groupby', 'limit', + ('rolling_type', 'rolling_periods'), + ] def render(self): if request.args.get("granularity") == "all": @@ -285,7 +227,6 @@ def render(self): values=metrics,) rolling_periods = request.args.get("rolling_periods") - limit = request.args.get("limit") rolling_type = request.args.get("rolling_type") if rolling_periods and rolling_type: if rolling_type == 'mean': @@ -306,17 +247,7 @@ def render(self): return super(TimeSeriesViz, self).render(chart_js=chart.javascript_cmd) def form_class(self): - limits = [0, 5, 10, 25, 50, 100, 500] - return form_factory(self.datasource, request.args, - extra_fields_dict={ - #'compare': TextField('Period Compare',), - 'rolling_type': SelectField( - 'Rolling', - choices=[(s, s) for s in ['mean', 'sum', 'std']]), - 'rolling_periods': TextField('Periods',), - 'limit': SelectField( - 'Series limit', choices=[(s, s) for s in limits]) - }) + return form_factory(self.datasource, self, request.args) def bake_query(self): """ @@ -324,14 +255,17 @@ def bake_query(self): """ return self.datasource.query(**self.query_obj()) + class TimeSeriesCompareViz(TimeSeriesViz): verbose_name = "Time Series - Percent Change" compare = 'percent' + class TimeSeriesCompareValueViz(TimeSeriesViz): verbose_name = "Time Series - Value Change" compare = 'value' + class TimeSeriesAreaViz(TimeSeriesViz): verbose_name = "Time Series - Stacked Area Chart" stacked=True