Dashboards
This commit is contained in:
parent
36351918c9
commit
a5b896414d
2
TODO.md
2
TODO.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
||||
|
|
|
|||
|
|
@ -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/")
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
Loading…
Reference in New Issue