Dashboards

This commit is contained in:
Maxime Beauchemin 2015-09-13 19:07:54 -07:00
parent 36351918c9
commit a5b896414d
8 changed files with 147 additions and 52 deletions

View File

@ -3,8 +3,6 @@
* DRUID: Allow for post aggregations (ratios!)
* compare time ranges
* csv export out of table view
* 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

View File

@ -10,7 +10,7 @@ from sqlalchemy import (
Column, Integer, String, ForeignKey, Text, Boolean, DateTime)
from panoramix.utils import JSONEncodedDict
from sqlalchemy import Table as sqlaTable
from sqlalchemy import create_engine, MetaData, desc, select, and_
from sqlalchemy import create_engine, MetaData, desc, select, and_, Table
from sqlalchemy.orm import relationship
from sqlalchemy.sql import table, literal_column, text
@ -33,12 +33,23 @@ class Slice(Model, AuditMixin):
__tablename__ = 'slices'
id = Column(Integer, primary_key=True)
slice_name = Column(String(250))
datasource_id = Column(Integer)
datasource_id = Column(Integer, ForeignKey('datasources.id'))
table_id = Column(Integer, ForeignKey('tables.id'))
datasource_type = Column(String(200))
datasource_name = Column(String(2000))
viz_type = Column(String(250))
params = Column(Text)
table = relationship('Table', backref='slices')
druid_datasource = relationship('Datasource', backref='slices')
def __repr__(self):
return self.slice_name
@property
def datasource(self):
return self.table or self.druid_datasource
@property
def slice_link(self):
d = json.loads(self.params)
@ -48,6 +59,53 @@ class Slice(Model, AuditMixin):
"{self.datasource_id}/?{kwargs}").format(**locals())
return '<a href="{url}">{self.slice_name}</a>'.format(**locals())
@property
def js_files(self):
from panoramix.viz import viz_types
return viz_types[self.viz_type].js_files
@property
def css_files(self):
from panoramix.viz import viz_types
return viz_types[self.viz_type].css_files
dashboard_slices = Table('dashboard_slices', Model.metadata,
Column('id', Integer, primary_key=True),
Column('dashboard_id', Integer, ForeignKey('dashboards.id')),
Column('slice_id', Integer, ForeignKey('slices.id')),
)
class Dashboard(Model, AuditMixin):
"""A dash to slash"""
__tablename__ = 'dashboards'
id = Column(Integer, primary_key=True)
dashboard_title = Column(String(500))
slices = relationship(
'Slice', secondary=dashboard_slices, backref='dashboards')
def __repr__(self):
return self.dashboard_title
def dashboard_link(self):
url = "/panoramix/dashboard/{}/".format(self.id)
return '<a href="{url}">{self.dashboard_title}</a>'.format(**locals())
@property
def js_files(self):
l = []
for o in self.slices:
l += o.js_files
return list(set(l))
@property
def css_files(self):
l = []
for o in self.slices:
l += o.css_files
return list(set(l))
class Queryable(object):
@property
@ -99,6 +157,9 @@ class Table(Model, Queryable, AuditMixin):
baselink = "tableview"
def __repr__(self):
return self.table_name
@property
def name(self):
return self.table_name

View File

@ -1,7 +1,7 @@
{% extends "panoramix/base.html" %}
{% block head_css %}
{{super()}}
{{ super() }}
<link rel="stylesheet" href="{{ url_for('static', filename="jquery.gridster.min.css") }}">
<style>
.gridster li {
@ -10,36 +10,40 @@
background: #EEEEEE;
overflow: auto;
box-shadow: 2px 2px 2px #AAA;
border-radius: 5px;
}
img.loading {
width: 20px;
margin: 5px;
}
.title {
text-align: center;
}
.slice_title {
text-align: center;
}
div.gridster {
visibility: hidden
}
</style>
{% endblock %}
{% block content_fluid %}
<div class="title"><h2>{{ dashboard.dashboard_title }}</h2></div>
<div class="gridster content_fluid">
<ul>
<li id="loadme3" data-row="1" data-col="1" data-sizex="1" data-sizey="1">
</li>
<li data-row="2" data-col="1" data-sizex="1" data-sizey="1"></li>
<li data-row="3" data-col="1" data-sizex="1" data-sizey="1"></li>
<li data-row="1" data-col="2" data-sizex="2" data-sizey="1"></li>
<li id="loadme1" data-row="2" data-col="2" data-sizex="2" data-sizey="2"></li>
<li data-row="1" data-col="4" data-sizex="1" data-sizey="1"></li>
<li data-row="2" data-col="4" data-sizex="2" data-sizey="1"></li>
<li data-row="3" data-col="4" data-sizex="1" data-sizey="1"></li>
<li data-row="1" data-col="5" data-sizex="1" data-sizey="1"></li>
<li data-row="3" data-col="5" data-sizex="1" data-sizey="1"></li>
<li data-row="1" data-col="6" data-sizex="1" data-sizey="1"></li>
<li data-row="2" data-col="6" data-sizex="1" data-sizey="2"></li>
<li id="loadme2" data-row="4" data-col="1" data-sizex="2" data-sizey="2">
{% for slice in dashboard.slices %}
<li
id="slice_{{ slice.id }}"
data-row="1"
data-col="{{ loop.index }}"
data-sizex="2"
data-sizey="2">
<div class="slice_title">
<h5>{{ slice.slice_name }}</h5>
</div>
</li>
{% endfor %}
</ul>
</div>
{% endblock %}
@ -48,15 +52,16 @@
{{ super() }}
<script src="{{ url_for("static", filename="jquery.gridster.with-extras.min.js") }}"></script>
<script>
$(".gridster li").html('<img src="/static/loading.gif" class="loading">');
//$(".gridster li").html('<img src="/static/loading.gif" class="loading">');
$(".gridster ul").gridster({
widget_margins: [5, 5],
widget_base_dimensions: [200, 200],
widget_base_dimensions: [150, 150],
resize: {enabled: true}
});
$("#loadme1").load("/panoramix/table/2/?flt_col_0=gender&datasource_id=2&flt_op_0=in&viz_type=table&row_limit=50&since=50%20years%20ago&until=now&metrics=total&granularity=all&datasource_name=baby_names&slice_name=Top%2050%20table&where=&groupby=name&flt_eq_0=&datasource_type=table&standalone=true");
//$("#loadme2").load("/panoramix/table/2/?flt_col_0=gender&datasource_id=2&flt_op_0=in&viz_type=pie&since=50%20years%20ago&until=now&metrics=total&limit=10&granularity=one%20day&datasource_name=baby_names&slice_name=Pie&where=&groupby=name&flt_eq_0=&datasource_type=table&standalone=true");
//$("loadme3").html('<iframe src="http://localhost:8088/panoramix/table/2/?flt_col_0=gender&datasource_id=2&flt_op_0=in&viz_type=pie&since=50%20years%20ago&until=now&metrics=total&limit=10&granularity=one%20day&datasource_name=baby_names&slice_name=Pie&where=&groupby=name&flt_eq_0=&datasource_type=table&standalone=true"></iframe>');
$("div.gridster").css('visibility', 'visible');
</script>
{% endblock %}

View File

@ -3,3 +3,18 @@
{% else %}
{% extends 'panoramix/datasource.html' %}
{% endif %}
{% block head %}
{{super()}}
{% for css in viz.css_files %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename=css) }}">
{% endfor %}
{% endblock %}
{% block tail %}
{{super()}}
{% for js in viz.js_files %}
<script src="{{ url_for('static', filename=js) }}"></script>
{% endfor %}
{% endblock %}

View File

@ -6,12 +6,6 @@
{% block tail %}
{{ super() }}
{% if viz.stockchart %}
<script src="{{ url_for("static", filename="highstock.js") }}"></script>
{% else %}
<script src="{{ url_for("static", filename="highcharts.js") }}"></script>
{% endif %}
<script src="{{ url_for("static", filename="highcharts-more.js") }}"></script>
<script>
$( document ).ready(function() {
Highcharts.setOptions({

View File

@ -1,10 +1,5 @@
{% extends "panoramix/viz.html" %}
{% block head_css %}
{{super()}}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='dataTables.bootstrap.css') }}">
{% endblock %}
{% block viz %}
{{ super() }}
{% if not error_msg %}
@ -36,18 +31,15 @@
{% endblock %}
{% block tail %}
{{ super() }}
<script src="{{ url_for('static', filename='jquery.dataTables.min.js') }}"></script>
<script src="{{ url_for('static', filename='dataTables.bootstrap.js') }}"></script>
<script>
//$('table').css('background-color', 'white');
$(document).ready(function() {
var table = $('table').DataTable({
paging: false,
{{ super() }}
<script>
//$('table').css('background-color', 'white');
$(document).ready(function() {
var table = $('table').DataTable({
paging: false,
});
table.column('-1').order( 'desc' ).draw();
});
table.column('-1').order( 'desc' ).draw();
});
</script>
</script>
{% endblock %}

View File

@ -106,7 +106,7 @@ appbuilder.add_view(
class SliceModelView(ModelView, DeleteMixin):
datamodel = SQLAInterface(models.Slice)
list_columns = ['slice_link', 'viz_type', 'created_by']
list_columns = ['slice_link', 'viz_type', 'datasource', 'created_by']
appbuilder.add_view(
SliceModelView,
@ -116,6 +116,21 @@ appbuilder.add_view(
category_icon='',)
class DashboardModelView(ModelView, DeleteMixin):
datamodel = SQLAInterface(models.Dashboard)
list_columns = ['dashboard_link', 'created_by']
edit_columns = ['dashboard_title', 'slices',]
add_columns = edit_columns
appbuilder.add_view(
DashboardModelView,
"Dashboards",
icon="fa-dashboard",
category="",
category_icon='',)
class DatabaseView(ModelView, DeleteMixin):
datamodel = SQLAInterface(models.Database)
list_columns = ['database_name']
@ -258,9 +273,17 @@ class Panoramix(BaseView):
return "super!"
@has_access
@expose("/dashboard/")
def dashboard(self):
return self.render_template("panoramix/dashboard.html")
@expose("/dashboard/<id_>/")
def dashboard(self, id_):
session = db.session()
dashboard = (
session
.query(models.Dashboard)
.filter(models.Dashboard.id == id_)
.first()
)
return self.render_template(
"panoramix/dashboard.html", dashboard=dashboard)
@has_access
@expose("/refresh_datasources/")

View File

@ -24,6 +24,8 @@ class BaseViz(object):
form_fields = [
'viz_type', 'metrics', 'groupby', 'granularity',
('since', 'until')]
js_files = []
css_files = []
def __init__(self, datasource, form_data, view):
self.datasource = datasource
@ -137,6 +139,8 @@ class TableViz(BaseViz):
verbose_name = "Table View"
template = 'panoramix/viz_table.html'
form_fields = BaseViz.form_fields + ['row_limit']
css_files = ['dataTables.bootstrap.css']
js_files = ['jquery.dataTables.min.js', 'dataTables.bootstrap.js']
def query_obj(self):
d = super(TableViz, self).query_obj()
@ -165,6 +169,7 @@ class HighchartsViz(BaseViz):
stacked = False
chart_type = 'not_stock'
compare = False
js_files = ['highcharts.js']
class BubbleViz(HighchartsViz):
@ -174,6 +179,7 @@ class BubbleViz(HighchartsViz):
form_fields = [
'viz_type', 'since', 'until',
'series', 'entity', 'x', 'y', 'size', 'limit']
js_files = ['highcharts.js', 'highcharts-more.js']
def query_obj(self):
d = super(BubbleViz, self).query_obj()
@ -212,6 +218,7 @@ class TimeSeriesViz(HighchartsViz):
chart_type = "spline"
stockchart = True
sort_legend_y = True
js_files = ['highstock.js', 'highcharts-more.js']
form_fields = [
'viz_type',
'granularity', ('since', 'until'),