diff --git a/TODO.md b/TODO.md
index 40c646ac8..e15ee80c4 100644
--- a/TODO.md
+++ b/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
-
diff --git a/panoramix/models.py b/panoramix/models.py
index b3294ae1a..4eb47a5a9 100644
--- a/panoramix/models.py
+++ b/panoramix/models.py
@@ -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 '{self.slice_name}'.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 '{self.dashboard_title}'.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
diff --git a/panoramix/templates/panoramix/dashboard.html b/panoramix/templates/panoramix/dashboard.html
index c65aff43b..ad8dfa0e8 100644
--- a/panoramix/templates/panoramix/dashboard.html
+++ b/panoramix/templates/panoramix/dashboard.html
@@ -1,7 +1,7 @@
{% extends "panoramix/base.html" %}
{% block head_css %}
- {{super()}}
+ {{ super() }}
{% endblock %}
{% block content_fluid %}
+
{{ dashboard.dashboard_title }}
{% endblock %}
@@ -48,15 +52,16 @@
{{ super() }}
{% endblock %}
diff --git a/panoramix/templates/panoramix/viz.html b/panoramix/templates/panoramix/viz.html
index f4803c8f9..eec9f330d 100644
--- a/panoramix/templates/panoramix/viz.html
+++ b/panoramix/templates/panoramix/viz.html
@@ -3,3 +3,18 @@
{% else %}
{% extends 'panoramix/datasource.html' %}
{% endif %}
+
+{% block head %}
+ {{super()}}
+ {% for css in viz.css_files %}
+
+ {% endfor %}
+{% endblock %}
+
+
+{% block tail %}
+ {{super()}}
+ {% for js in viz.js_files %}
+
+ {% endfor %}
+{% endblock %}
diff --git a/panoramix/templates/panoramix/viz_highcharts.html b/panoramix/templates/panoramix/viz_highcharts.html
index ae10b83e7..5d815d464 100644
--- a/panoramix/templates/panoramix/viz_highcharts.html
+++ b/panoramix/templates/panoramix/viz_highcharts.html
@@ -6,12 +6,6 @@
{% block tail %}
{{ super() }}
- {% if viz.stockchart %}
-
- {% else %}
-
- {% endif %}
-
-
-
-
+
{% endblock %}
diff --git a/panoramix/views.py b/panoramix/views.py
index b3a3f1815..1be93d7a7 100644
--- a/panoramix/views.py
+++ b/panoramix/views.py
@@ -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//")
+ 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/")
diff --git a/panoramix/viz.py b/panoramix/viz.py
index cc8f6760a..4a38b8f79 100644
--- a/panoramix/viz.py
+++ b/panoramix/viz.py
@@ -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'),