Merge pull request #25 from mistercrunch/dash
Adding basic dashboarding support!
This commit is contained in:
commit
c1f28a3e74
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
|
||||
|
||||
|
|
|
|||
|
|
@ -9,12 +9,16 @@ class BaseHighchart(object):
|
|||
target_div = 'chart'
|
||||
|
||||
@property
|
||||
def javascript_cmd(self):
|
||||
def json(self):
|
||||
js = dumps(self.chart)
|
||||
js = (
|
||||
return (
|
||||
js.replace('"{{TOOLTIP_FORMATTER}}"', self.tooltip_formatter)
|
||||
.replace("\n", " ")
|
||||
)
|
||||
|
||||
@property
|
||||
def javascript_cmd(self):
|
||||
js = self.json
|
||||
if self.stockchart:
|
||||
return "new Highcharts.StockChart(%s);" % js
|
||||
return "new Highcharts.Chart(%s);" % js
|
||||
|
|
@ -83,7 +87,7 @@ class Highchart(BaseHighchart):
|
|||
if sort_legend_y:
|
||||
if 'tooltip' not in chart:
|
||||
chart['tooltip'] = {
|
||||
'formatter': "{{TOOLTIP_FORMATTER}}"
|
||||
#'formatter': "{{TOOLTIP_FORMATTER}}"
|
||||
}
|
||||
if self.zoom:
|
||||
chart["zoomType"] = self.zoom
|
||||
|
|
@ -192,7 +196,7 @@ class Highchart(BaseHighchart):
|
|||
|
||||
|
||||
class HighchartBubble(BaseHighchart):
|
||||
def __init__(self, df, target_div='chart', height=800):
|
||||
def __init__(self, df, target_div=None, height=None):
|
||||
self.df = df
|
||||
self.chart = {
|
||||
'chart': {
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ from pydruid import client
|
|||
from pydruid.utils.filters import Dimension, Filter
|
||||
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
|
||||
from flask import request
|
||||
|
||||
from copy import deepcopy, copy
|
||||
from collections import namedtuple
|
||||
|
|
@ -22,8 +22,8 @@ import sqlparse
|
|||
import requests
|
||||
import textwrap
|
||||
|
||||
from panoramix import db, get_session
|
||||
import config
|
||||
from panoramix import db, get_session, config, utils
|
||||
from panoramix.viz import viz_types
|
||||
|
||||
QueryResult = namedtuple('namedtuple', ['df', 'query', 'duration'])
|
||||
|
||||
|
|
@ -32,9 +32,104 @@ class Slice(Model, AuditMixin):
|
|||
"""A slice is essentially a report or a view on data"""
|
||||
__tablename__ = 'slices'
|
||||
id = Column(Integer, primary_key=True)
|
||||
params = Column(JSONEncodedDict)
|
||||
datasource = Column(String(250))
|
||||
slice_name = Column(String(250))
|
||||
druid_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', foreign_keys=[table_id], backref='slices')
|
||||
druid_datasource = relationship(
|
||||
'Datasource', foreign_keys=[druid_datasource_id], backref='slices')
|
||||
|
||||
def __repr__(self):
|
||||
return self.slice_name
|
||||
|
||||
@property
|
||||
def datasource(self):
|
||||
return self.table or self.druid_datasource
|
||||
|
||||
@property
|
||||
@utils.memoized
|
||||
def viz(self):
|
||||
d = json.loads(self.params)
|
||||
viz = viz_types[self.viz_type](
|
||||
self.datasource,
|
||||
form_data=d)
|
||||
return viz
|
||||
|
||||
@property
|
||||
def datasource_id(self):
|
||||
datasource = self.datasource
|
||||
return datasource.id if datasource else None
|
||||
|
||||
@property
|
||||
def slice_url(self):
|
||||
d = json.loads(self.params)
|
||||
from werkzeug.urls import Href
|
||||
href = Href(
|
||||
"/panoramix/{self.datasource_type}/"
|
||||
"{self.datasource_id}/".format(self=self))
|
||||
return href(d)
|
||||
|
||||
@property
|
||||
def slice_link(self):
|
||||
url = self.slice_url
|
||||
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
|
||||
|
||||
def get_viz(self):
|
||||
pass
|
||||
|
||||
|
||||
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))
|
||||
position_json = Column(Text)
|
||||
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 += [f for f in o.js_files if f not in l]
|
||||
return l
|
||||
|
||||
@property
|
||||
def css_files(self):
|
||||
l = []
|
||||
for o in self.slices:
|
||||
l += o.css_files
|
||||
return list(set(l))
|
||||
|
||||
|
||||
class Queryable(object):
|
||||
|
|
@ -72,6 +167,8 @@ class Database(Model, AuditMixin):
|
|||
|
||||
|
||||
class Table(Model, Queryable, AuditMixin):
|
||||
type = "table"
|
||||
|
||||
__tablename__ = 'tables'
|
||||
id = Column(Integer, primary_key=True)
|
||||
table_name = Column(String(255), unique=True)
|
||||
|
|
@ -85,6 +182,9 @@ class Table(Model, Queryable, AuditMixin):
|
|||
|
||||
baselink = "tableview"
|
||||
|
||||
def __repr__(self):
|
||||
return self.table_name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.table_name
|
||||
|
|
@ -446,6 +546,7 @@ class Cluster(Model, AuditMixin):
|
|||
|
||||
|
||||
class Datasource(Model, AuditMixin, Queryable):
|
||||
type = "datasource"
|
||||
|
||||
baselink = "datasourcemodelview"
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
/*! gridster.js - v0.5.6 - 2014-09-25 - * http://gridster.net/ - Copyright (c) 2014 ducksboard; Licensed MIT */
|
||||
.gridster{position:relative}.gridster>*{margin:0 auto;-webkit-transition:height .4s,width .4s;-moz-transition:height .4s,width .4s;-o-transition:height .4s,width .4s;-ms-transition:height .4s,width .4s;transition:height .4s,width .4s}.gridster .gs-w{z-index:2;position:absolute}.ready .gs-w:not(.preview-holder){-webkit-transition:opacity .3s,left .3s,top .3s;-moz-transition:opacity .3s,left .3s,top .3s;-o-transition:opacity .3s,left .3s,top .3s;transition:opacity .3s,left .3s,top .3s}.ready .gs-w:not(.preview-holder),.ready .resize-preview-holder{-webkit-transition:opacity .3s,left .3s,top .3s,width .3s,height .3s;-moz-transition:opacity .3s,left .3s,top .3s,width .3s,height .3s;-o-transition:opacity .3s,left .3s,top .3s,width .3s,height .3s;transition:opacity .3s,left .3s,top .3s,width .3s,height .3s}.gridster .preview-holder{z-index:1;position:absolute;background-color:#fff;border-color:#fff;opacity:.3}.gridster .player-revert{z-index:10!important;-webkit-transition:left .3s,top .3s!important;-moz-transition:left .3s,top .3s!important;-o-transition:left .3s,top .3s!important;transition:left .3s,top .3s!important}.gridster .dragging,.gridster .resizing{z-index:10!important;-webkit-transition:all 0s!important;-moz-transition:all 0s!important;-o-transition:all 0s!important;transition:all 0s!important}.gs-resize-handle{position:absolute;z-index:1}.gs-resize-handle-both{width:20px;height:20px;bottom:-8px;right:-8px;background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg08IS0tIEdlbmVyYXRvcjogQWRvYmUgRmlyZXdvcmtzIENTNiwgRXhwb3J0IFNWRyBFeHRlbnNpb24gYnkgQWFyb24gQmVhbGwgKGh0dHA6Ly9maXJld29ya3MuYWJlYWxsLmNvbSkgLiBWZXJzaW9uOiAwLjYuMSAgLS0+DTwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DTxzdmcgaWQ9IlVudGl0bGVkLVBhZ2UlMjAxIiB2aWV3Qm94PSIwIDAgNiA2IiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHZlcnNpb249IjEuMSINCXhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiDQl4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjZweCIgaGVpZ2h0PSI2cHgiDT4NCTxnIG9wYWNpdHk9IjAuMzAyIj4NCQk8cGF0aCBkPSJNIDYgNiBMIDAgNiBMIDAgNC4yIEwgNCA0LjIgTCA0LjIgNC4yIEwgNC4yIDAgTCA2IDAgTCA2IDYgTCA2IDYgWiIgZmlsbD0iIzAwMDAwMCIvPg0JPC9nPg08L3N2Zz4=);background-position:top left;background-repeat:no-repeat;cursor:se-resize;z-index:20}.gs-resize-handle-x{top:0;bottom:13px;right:-5px;width:10px;cursor:e-resize}.gs-resize-handle-y{left:0;right:13px;bottom:-5px;height:10px;cursor:s-resize}.gs-w:hover .gs-resize-handle,.resizing .gs-resize-handle{opacity:1}.gs-resize-handle,.gs-w.dragging .gs-resize-handle{opacity:0}.gs-resize-disabled .gs-resize-handle{display:none!important}[data-max-sizex="1"] .gs-resize-handle-x,[data-max-sizey="1"] .gs-resize-handle-y,[data-max-sizey="1"][data-max-sizex="1"] .gs-resize-handle{display:none!important}
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -0,0 +1,166 @@
|
|||
{% extends "panoramix/base.html" %}
|
||||
|
||||
{% block head_css %}
|
||||
{{ super() }}
|
||||
{% for css in dashboard.css_files %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename=css) }}">
|
||||
{% endfor %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename="jquery.gridster.min.css") }}">
|
||||
<style>
|
||||
a i{
|
||||
cursor: pointer;
|
||||
}
|
||||
i.drag{
|
||||
cursor: move; !important
|
||||
}
|
||||
.gridster .preview-holder {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
background-color: #AAA;
|
||||
border-color: #AAA;
|
||||
opacity: 0.3;
|
||||
}
|
||||
.gridster li {
|
||||
list-style-type: none;
|
||||
border: 1px solid gray;
|
||||
overflow: auto;
|
||||
box-shadow: 2px 2px 2px #AAA;
|
||||
border-radius: 5px;
|
||||
background-color: white;
|
||||
}
|
||||
.gridster .dragging,
|
||||
.gridster .resizing {
|
||||
opacity: 0.5;
|
||||
}
|
||||
img.loading {
|
||||
width: 20px;
|
||||
margin: 5px;
|
||||
}
|
||||
.title {
|
||||
text-align: center;
|
||||
}
|
||||
.slice_title {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
padding: 5px;
|
||||
}
|
||||
div.gridster {
|
||||
visibility: hidden
|
||||
}
|
||||
div.slice_content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content_fluid %}
|
||||
<div class="title">
|
||||
<div class="row">
|
||||
<div class="col-md-1 text-left"></div>
|
||||
<div class="col-md-10 text-middle">
|
||||
<h2>
|
||||
{{ dashboard.dashboard_title }}
|
||||
<a id="savedash"><i class="fa fa-save"></i></a>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-md-1 text-right">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gridster content_fluid">
|
||||
<ul>
|
||||
{% for slice in dashboard.slices %}
|
||||
{% set pos = pos_dict.get(slice.id, {}) %}
|
||||
{% set viz = slice.viz %}
|
||||
{% import viz.template as viz_macros %}
|
||||
<li
|
||||
id="slice_{{ slice.id }}"
|
||||
slice_id="{{ slice.id }}"
|
||||
data-row="{{ pos.row or 1 }}"
|
||||
data-col="{{ pos.col or loop.index }}"
|
||||
data-sizex="{{ pos.size_x or 4 }}"
|
||||
data-sizey="{{ pos.size_y or 4 }}">
|
||||
<div class="slice_title" style="height: 0px;">
|
||||
<div class="row">
|
||||
<div class="col-md-4 text-left">
|
||||
<a>
|
||||
<i class="fa fa-arrows drag"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-4 text-middle">
|
||||
<span>{{ slice.slice_name }}</span>
|
||||
</div>
|
||||
<div class="col-md-4 text-right" style="position: relative;">
|
||||
<a href="{{ slice.slice_url }}"><i class="fa fa-play"></i></a>
|
||||
<a class="refresh"><i class="fa fa-refresh"></i></a>
|
||||
<a><i class="fa fa-gear"></i></a>
|
||||
<a class="closewidget"><i class="fa fa-close"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ viz_macros.viz_html(viz) }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block tail %}
|
||||
{{ super() }}
|
||||
{% for js in dashboard.js_files %}
|
||||
<script src="{{ url_for('static', filename=js) }}"></script>
|
||||
{% endfor %}
|
||||
<script src="{{ url_for("static", filename="jquery.gridster.with-extras.min.js") }}"></script>
|
||||
<script src="{{ url_for("static", filename="d3.min.js") }}"></script>
|
||||
<script>
|
||||
f = d3.format(".4s");
|
||||
</script>
|
||||
<script>
|
||||
$( document ).ready(function() {
|
||||
var gridster = $(".gridster ul").gridster({
|
||||
widget_margins: [5, 5],
|
||||
widget_base_dimensions: [100, 100],
|
||||
draggable: {
|
||||
handle: '.drag',
|
||||
},
|
||||
resize: {
|
||||
enabled: true,
|
||||
stop: function(e, ui, $widget) {
|
||||
$widget.find("a.refresh").click();
|
||||
}
|
||||
},
|
||||
serialize_params:function($w, wgd) {
|
||||
return {
|
||||
slice_id: $($w).attr('slice_id'),
|
||||
col: wgd.col,
|
||||
row: wgd.row,
|
||||
size_x: wgd.size_x,
|
||||
size_y: wgd.size_y
|
||||
};
|
||||
},
|
||||
}).data('gridster');
|
||||
$("div.gridster").css('visibility', 'visible');
|
||||
$("#savedash").click(function(){
|
||||
var data = gridster.serialize();
|
||||
console.log(data);
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: '/panoramix/save_dash/{{ dashboard.id }}/',
|
||||
data: {data: JSON.stringify(data)},
|
||||
success: function(){console.log('Sucess!')},
|
||||
});
|
||||
});
|
||||
$("a.closewidget").click(function(){
|
||||
var li = $(this).parents("li");
|
||||
gridster.remove_widget(li);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% for slice in dashboard.slices %}
|
||||
{% set viz = slice.viz %}
|
||||
{% import viz.template as viz_macros %}
|
||||
{{ viz_macros.viz_js(viz) }}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
|
@ -1,36 +1,40 @@
|
|||
{% extends "panoramix/base.html" %}
|
||||
{% block head_css %}
|
||||
{{super()}}
|
||||
{% set datasource = viz.datasource %}
|
||||
<style>
|
||||
.select2-container-multi .select2-choices {
|
||||
height: 70px;
|
||||
overflow: auto;
|
||||
}
|
||||
.no-gutter > [class*='col-'] {
|
||||
.select2-container-multi .select2-choices {
|
||||
height: 70px;
|
||||
overflow: auto;
|
||||
}
|
||||
.no-gutter > [class*='col-'] {
|
||||
padding-right:0;
|
||||
padding-left:0;
|
||||
}
|
||||
form div.form-control {
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
form input.form-control {
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
.tooltip-inner {
|
||||
max-width: 350px;
|
||||
//width: 350px;
|
||||
}
|
||||
}
|
||||
form div.form-control {
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
form input.form-control {
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
.tooltip-inner {
|
||||
max-width: 350px;
|
||||
//width: 350px;
|
||||
}
|
||||
img.loading {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content_fluid %}
|
||||
{% set datasource = viz.datasource %}
|
||||
{% set form = viz.form %}
|
||||
<div class="container-fluid">
|
||||
<div class="col-md-3">
|
||||
<h3>
|
||||
{{ datasource.name }}
|
||||
{% if datasource.description %}
|
||||
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="bottom" title="{{ datasource.description }}"></i>
|
||||
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="bottom" title="{{ datasource.description }}"></i>
|
||||
{% endif %}
|
||||
<a href="/{{ datasource.baselink }}/edit/{{ datasource.id }}">
|
||||
<i class="fa fa-edit"></i>
|
||||
|
|
@ -40,94 +44,95 @@ form input.form-control {
|
|||
<hr>
|
||||
<form id="query" method="GET" style="display: none;">
|
||||
{% for fieldname in form.field_order %}
|
||||
{% if not fieldname.__iter__ %}
|
||||
<div>
|
||||
{% set field = form.get_field(fieldname)%}
|
||||
<div>
|
||||
{{ field.label }}
|
||||
{% if field.description %}
|
||||
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="right"
|
||||
title="{{ field.description }}"></i>
|
||||
{% endif %}:
|
||||
</div>
|
||||
<div>
|
||||
{{ field(class_=form.field_css_classes(field.name)) }}
|
||||
</div>
|
||||
</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 }}
|
||||
{% if field.description %}
|
||||
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="right"
|
||||
title="{{ field.description }}"></i>
|
||||
{% endif %}:
|
||||
{{ field(class_=form.field_css_classes(field.name)) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if not fieldname.__iter__ %}
|
||||
<div>
|
||||
{% set field = form.get_field(fieldname)%}
|
||||
<div>
|
||||
{{ field.label }}
|
||||
{% if field.description %}
|
||||
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="right"
|
||||
title="{{ field.description }}"></i>
|
||||
{% endif %}:
|
||||
</div>
|
||||
<div>
|
||||
{{ field(class_=form.field_css_classes(field.name)) }}
|
||||
</div>
|
||||
</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 }}
|
||||
{% if field.description %}
|
||||
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="right"
|
||||
title="{{ field.description }}"></i>
|
||||
{% endif %}:
|
||||
{{ field(class_=form.field_css_classes(field.name)) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<hr>
|
||||
<h4>Filters</h4>
|
||||
<div id="flt0" style="display: none;">
|
||||
<span class="">{{ form.flt_col_0(class_="form-control inc") }}</span>
|
||||
<div class="row">
|
||||
<span class="col col-sm-4">{{ form.flt_op_0(class_="form-control inc") }}</span>
|
||||
<span class="col col-sm-6">{{ form.flt_eq_0(class_="form-control inc") }}</span>
|
||||
<button type="button" class="btn btn-sm remove" aria-label="Delete filter">
|
||||
<span class="glyphicon glyphicon-minus" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
<hr style="margin: 5px 0px;"/>
|
||||
<div id="flt0" style="display: none;">
|
||||
<span class="">{{ form.flt_col_0(class_="form-control inc") }}</span>
|
||||
<div class="row">
|
||||
<span class="col col-sm-4">{{ form.flt_op_0(class_="form-control inc") }}</span>
|
||||
<span class="col col-sm-6">{{ form.flt_eq_0(class_="form-control inc") }}</span>
|
||||
<button type="button" class="btn btn-sm remove" aria-label="Delete filter">
|
||||
<span class="glyphicon glyphicon-minus" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
<hr style="margin: 5px 0px;"/>
|
||||
</div>
|
||||
<div id="filters"></div>
|
||||
<button type="button" id="plus" class="btn btn-sm" aria-label="Add a filter">
|
||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||
</button>
|
||||
<hr>
|
||||
<button type="button" class="btn btn-primary" id="druidify">Druidify!</button>
|
||||
<button type="button" class="btn btn-default" id="bookmark">Bookmark</button>
|
||||
<button type="button" class="btn btn-primary" id="druidify">
|
||||
<i class="fa fa-bolt"></i>
|
||||
Druidify!
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" id="save">
|
||||
<i class="fa fa-save"></i>
|
||||
Save as Slice
|
||||
</button>
|
||||
<hr style="margin-bottom: 0px;">
|
||||
<img src="{{ url_for("static", filename="tux_panoramix.png") }}" width=250>
|
||||
<input type="hidden" id="slice_name" name="slice_name" value="TEST">
|
||||
<input type="hidden" name="datasource_name" value="{{ datasource.name }}">
|
||||
<input type="hidden" name="datasource_id" value="{{ datasource.id }}">
|
||||
<input type="hidden" name="datasource_type" value="{{ datasource.type }}">
|
||||
</form><br>
|
||||
</div>
|
||||
|
||||
<div class="col-md-9">
|
||||
<h3>{{ viz.verbose_name }}
|
||||
{% if results %}
|
||||
<span class="label label-success">
|
||||
{{ "{0:0.4f}".format(results.duration.total_seconds()) }} s
|
||||
</span>
|
||||
<span class="label label-info btn"
|
||||
data-toggle="modal" data-target="#query_modal">query</span>
|
||||
{% endif %}
|
||||
{% if False %}
|
||||
<span class="label label-success">
|
||||
{{ "{0:0.4f}".format(results.duration.total_seconds()) }} s
|
||||
</span>
|
||||
<span class="label label-info btn"
|
||||
data-toggle="modal" data-target="#query_modal">query</span>
|
||||
{% endif %}
|
||||
</h3>
|
||||
<hr/>
|
||||
{% block viz %}
|
||||
{% if viz.error_msg %}
|
||||
<div class="alert alert-danger">{{ viz.error_msg }}</div>
|
||||
{% endif %}
|
||||
{% if viz.warning_msg %}
|
||||
<div class="alert alert-warning">{{ viz.warning_msg }}</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
<div class="viz" style="height: 600px;">
|
||||
{% block viz_html %}
|
||||
{% if viz.error_msg %}
|
||||
<div class="alert alert-danger">{{ viz.error_msg }}</div>
|
||||
{% endif %}
|
||||
{% if viz.warning_msg %}
|
||||
<div class="alert alert-warning">{{ viz.warning_msg }}</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% if debug %}
|
||||
<h3>Results</h3>
|
||||
<pre>
|
||||
{{ results }}
|
||||
</pre>
|
||||
|
||||
<h3>Latest Segment Metadata</h3>
|
||||
<pre>
|
||||
{{ latest_metadata }}
|
||||
</pre>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="query_modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
|
||||
|
|
@ -138,7 +143,7 @@ form input.form-control {
|
|||
<h4 class="modal-title" id="myModalLabel">Query</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre>{{ results.query }}</pre>
|
||||
<pre>{{ '' }}</pre>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
|
|
@ -150,32 +155,30 @@ form input.form-control {
|
|||
|
||||
{% block tail_js %}
|
||||
{{ super() }}
|
||||
<script src="{{ url_for("static", filename="d3.min.js") }}"></script>
|
||||
<script>
|
||||
$( document ).ready(function() {
|
||||
f = d3.format(".4s");
|
||||
function getParam(name) {
|
||||
$( document ).ready(function() {
|
||||
function getParam(name) {
|
||||
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
|
||||
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
|
||||
results = regex.exec(location.search);
|
||||
results = regex.exec(location.search);
|
||||
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
|
||||
}
|
||||
}
|
||||
|
||||
$(".select2").select2();
|
||||
$("form").slideDown("slow");
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
$(".select2").select2();
|
||||
$("form").slideDown("slow");
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
|
||||
function set_filters(){
|
||||
for (var i=1; i<10; i++){
|
||||
var eq = getParam("flt_eq_" + i);
|
||||
if (eq !=''){
|
||||
add_filter(i);
|
||||
function set_filters(){
|
||||
for (var i=1; i<10; i++){
|
||||
var eq = getParam("flt_eq_" + i);
|
||||
if (eq !=''){
|
||||
add_filter(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
set_filters();
|
||||
set_filters();
|
||||
|
||||
function add_filter(i) {
|
||||
function add_filter(i) {
|
||||
cp = $("#flt0").clone();
|
||||
$(cp).appendTo("#filters");
|
||||
$(cp).slideDown("slow");
|
||||
|
|
@ -189,81 +192,85 @@ $( document ).ready(function() {
|
|||
$(cp).find('.remove').click(function() {
|
||||
$(this).parent().parent().slideUp("slow", function(){$(this).remove()});
|
||||
});
|
||||
}
|
||||
$("#plus").click(add_filter);
|
||||
$("#bookmark").click(function () {alert("Not implemented yet...");})
|
||||
add_filter();
|
||||
$("#druidify").click(function () {
|
||||
var i = 1;
|
||||
}
|
||||
$("#plus").click(add_filter);
|
||||
$("#save").click(function () {
|
||||
var slice_name = prompt("Name your slice!");
|
||||
$("#slice_name").val(slice_name);
|
||||
$.get( "/panoramix/save/", $("#query").serialize() );
|
||||
})
|
||||
add_filter();
|
||||
$("#druidify").click(function () {
|
||||
var i = 1;
|
||||
|
||||
// removing empty filters
|
||||
$("#filters > div").each(function(){
|
||||
if ($(this).find("#flt_eq_0").val() == '')
|
||||
// removing empty filters
|
||||
$("#filters > div").each(function(){
|
||||
if ($(this).find("#flt_eq_0").val() == '')
|
||||
$(this).slideUp();
|
||||
});
|
||||
});
|
||||
|
||||
// Assigning the right id to form elements in filters
|
||||
$("#filters > div").each(function(){
|
||||
$(this).attr("id", function(){return "flt_" + i;})
|
||||
$(this).find("#flt_col_0")
|
||||
// Assigning the right id to form elements in filters
|
||||
$("#filters > div").each(function(){
|
||||
$(this).attr("id", function(){return "flt_" + i;})
|
||||
$(this).find("#flt_col_0")
|
||||
.attr("id", function(){return "flt_col_" + i;})
|
||||
.attr("name", function(){return "flt_col_" + i;});
|
||||
$(this).find("#flt_op_0")
|
||||
$(this).find("#flt_op_0")
|
||||
.attr("id", function(){return "flt_op_" + i;})
|
||||
.attr("name", function(){return "flt_op_" + i;});
|
||||
$(this).find("#flt_eq_0")
|
||||
$(this).find("#flt_eq_0")
|
||||
.attr("id", function(){return "flt_eq_" + i;})
|
||||
.attr("name", function(){return "flt_eq_" + i;});
|
||||
i++;
|
||||
i++;
|
||||
});
|
||||
$("#query").submit();
|
||||
});
|
||||
$("#query").submit();
|
||||
});
|
||||
|
||||
function create_choices (term, data) {
|
||||
function create_choices (term, data) {
|
||||
if ($(data).filter(function() {
|
||||
return this.text.localeCompare(term)===0;
|
||||
return this.text.localeCompare(term)===0;
|
||||
}).length===0)
|
||||
{return {id:term, text:term};}
|
||||
}
|
||||
$(".select2_free_since").select2({
|
||||
}
|
||||
$(".select2_free_since").select2({
|
||||
createSearchChoice: create_choices,
|
||||
multiple: false,
|
||||
data: [
|
||||
{id: '-1 hour', text: '-1 hour'},
|
||||
{id: '-12 hours', text: '-12 hours'},
|
||||
{id: '-1 day', text: '-1 day'},
|
||||
{id: '-7 days', text: '-7 days'},
|
||||
{id: '-28 days', text: '-28 days'},
|
||||
{id: '-90 days', text: '-90 days'},
|
||||
{id: '{{ form.data.since }}', text: '{{ form.data.since }}'},
|
||||
{id: '-1 hour', text: '-1 hour'},
|
||||
{id: '-12 hours', text: '-12 hours'},
|
||||
{id: '-1 day', text: '-1 day'},
|
||||
{id: '-7 days', text: '-7 days'},
|
||||
{id: '-28 days', text: '-28 days'},
|
||||
{id: '-90 days', text: '-90 days'},
|
||||
{id: '{{ viz.form.data.since }}', text: '{{ viz.form.data.since }}'},
|
||||
]
|
||||
});
|
||||
$(".select2_free_until").select2({
|
||||
});
|
||||
$(".select2_free_until").select2({
|
||||
createSearchChoice: create_choices,
|
||||
multiple: false,
|
||||
data: [
|
||||
{id: '{{ form.data.until }}', text: '{{ form.data.until }}'},
|
||||
{id: 'now', text: 'now'},
|
||||
{id: '-1 day', text: '-1 day'},
|
||||
{id: '-7 days', text: '-7 days'},
|
||||
{id: '-28 days', text: '-28 days'},
|
||||
{id: '-90 days', text: '-90 days'},
|
||||
{id: '{{ viz.form.data.until }}', text: '{{ viz.form.data.until }}'},
|
||||
{id: 'now', text: 'now'},
|
||||
{id: '-1 day', text: '-1 day'},
|
||||
{id: '-7 days', text: '-7 days'},
|
||||
{id: '-28 days', text: '-28 days'},
|
||||
{id: '-90 days', text: '-90 days'},
|
||||
]
|
||||
});
|
||||
$(".select2_free_granularity").select2({
|
||||
});
|
||||
$(".select2_free_granularity").select2({
|
||||
createSearchChoice: create_choices,
|
||||
multiple: false,
|
||||
data: [
|
||||
{id: '{{ form.data.granularity }}', text: '{{ form.data.granularity }}'},
|
||||
{id: 'all', text: 'all'},
|
||||
{id: '5 seconds', text: '5 seconds'},
|
||||
{id: '30 seconds', text: '30 seconds'},
|
||||
{id: '1 minute', text: '1 minute'},
|
||||
{id: '5 minutes', text: '5 minutes'},
|
||||
{id: '1 day', text: '1 day'},
|
||||
{id: '7 days', text: '7 days'},
|
||||
{id: '{{ viz.form.data.granularity }}', text: '{{ viz.form.data.granularity }}'},
|
||||
{id: 'all', text: 'all'},
|
||||
{id: '5 seconds', text: '5 seconds'},
|
||||
{id: '30 seconds', text: '30 seconds'},
|
||||
{id: '1 minute', text: '1 minute'},
|
||||
{id: '5 minutes', text: '5 minutes'},
|
||||
{id: '1 day', text: '1 day'},
|
||||
{id: '7 days', text: '7 days'},
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
{% import viz.template as viz_macros %}
|
||||
|
||||
{% if viz.args.get("json") == "true" %}
|
||||
{{ viz.get_json() }}
|
||||
{% else %}
|
||||
{% if viz.args.get("standalone") == "true" %}
|
||||
{% extends 'panoramix/viz_standalone.html' %}
|
||||
{% else %}
|
||||
{% extends 'panoramix/datasource.html' %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% block viz_html %}
|
||||
{{ viz_macros.viz_html(viz) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block head_css %}
|
||||
{{super()}}
|
||||
{% if viz.args.get("skip_libs") != "true" %}
|
||||
{% for css in viz.css_files %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename=css) }}">
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{{ viz_macros.viz_css(viz) }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block tail %}
|
||||
{{super()}}
|
||||
{% if viz.args.get("skip_libs") != "true" %}
|
||||
<script src="{{ url_for("static", filename="d3.min.js") }}"></script>
|
||||
<script>
|
||||
f = d3.format(".4s");
|
||||
</script>
|
||||
{% for js in viz.js_files %}
|
||||
<script src="{{ url_for('static', filename=js) }}"></script>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{{ viz_macros.viz_js(viz) }}
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
|
|
@ -1,34 +1,48 @@
|
|||
{% extends "panoramix/datasource.html" %}
|
||||
{% block viz %}
|
||||
{{ super() }}
|
||||
<div id="chart"></div>
|
||||
{% endblock %}
|
||||
{% macro viz_html(viz) %}
|
||||
<div id="{{ viz.token }}" style="height:100%; width: 100%">
|
||||
<img src="{{ url_for("static", filename="loading.gif") }}" class="loading">
|
||||
<div class="chart" style="height:100%; width: 100%"></div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% 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>
|
||||
{% macro viz_js(viz) %}
|
||||
<script>
|
||||
$( document ).ready(function() {
|
||||
$( document ).ready(function() {
|
||||
Highcharts.setOptions({
|
||||
colors: [
|
||||
"#FF5A5F", "#007A87", "#7B0051", "#00D1C1", "#8CE071", "#FFB400",
|
||||
"#FFAA91", "#B4A76C", "#9CA299", "#565A5C"
|
||||
],
|
||||
global: {
|
||||
useUTC: false
|
||||
},
|
||||
colors: [
|
||||
"#FF5A5F", "#007A87", "#7B0051", "#00D1C1", "#8CE071", "#FFB400",
|
||||
"#FFAA91", "#B4A76C", "#9CA299", "#565A5C"
|
||||
],
|
||||
global: {
|
||||
useUTC: false
|
||||
},
|
||||
});
|
||||
$("#viz_type").click(function(){
|
||||
$("#queryform").submit();
|
||||
})
|
||||
{% if chart_js %}
|
||||
{{ chart_js|safe }}
|
||||
{% endif %}
|
||||
});
|
||||
var token = $("#{{ viz.token }}");
|
||||
var loading = $("#{{ viz.token }}").find("img.loading");
|
||||
var chart = $("#{{ viz.token }}").find("div.chart");
|
||||
var refresh = function(){
|
||||
chart.hide();
|
||||
loading.show();
|
||||
var url = "{{ viz.get_url(json="true")|safe }}";
|
||||
$.getJSON(url, function(data){
|
||||
chart.width(token.width());
|
||||
chart.height(token.height()-40);
|
||||
chart.highcharts('{{ viz.chart_call }}', data);
|
||||
chart.show();
|
||||
token.find("img.loading").hide();
|
||||
})
|
||||
.fail(function(xhr) {
|
||||
var err = '<div class="alert alert-danger">' + xhr.responseText + '</div>';
|
||||
loading.hide();
|
||||
chart.show();
|
||||
chart.html(err);
|
||||
});
|
||||
};
|
||||
refresh();
|
||||
token.parent().find("a.refresh").click(refresh);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro viz_css(viz) %}
|
||||
{% endmacro %}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<html>
|
||||
<head>
|
||||
{% if viz.args.get("skip_libs") != "true" %}
|
||||
{% block head %}
|
||||
<script src="{{url_for('appbuilder.static',filename='js/jquery-latest.js')}}"></script>
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
{% block tail %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block viz_html %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,51 +1,54 @@
|
|||
{% extends "panoramix/datasource.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 %}
|
||||
<table class="dataframe table table-striped table-bordered table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
{% for col in df.columns if not col.endswith('__perc') %}
|
||||
<th>{{ col }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in df.to_dict(orient="records") %}
|
||||
<tr>
|
||||
{% for col in df.columns if not col.endswith('__perc') %}
|
||||
{% if col + '__perc' in df.columns %}
|
||||
<td style="background-image: linear-gradient(to right, lightgrey, lightgrey {{ row[col+'__perc'] }}%, rgba(0,0,0,0) {{ row[col+'__perc'] }}%">{{ row[col] }}</td>
|
||||
{% else %}
|
||||
<td>{{ row[col] }}</td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% macro viz_html(viz) %}
|
||||
{% if viz.args.get("async") == "true" %}
|
||||
{% set df = viz.get_df() %}
|
||||
<table class="dataframe table table-striped table-bordered table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
{% for col in df.columns if not col.endswith('__perc') %}
|
||||
<th>{{ col }}</th>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in df.to_dict(orient="records") %}
|
||||
<tr>
|
||||
{% for col in df.columns if not col.endswith('__perc') %}
|
||||
{% if col + '__perc' in df.columns %}
|
||||
<td style="background-image: linear-gradient(to right, lightgrey, lightgrey {{ row[col+'__perc'] }}%, rgba(0,0,0,0) {{ row[col+'__perc'] }}%">
|
||||
{{ row[col] }}
|
||||
</td>
|
||||
{% else %}
|
||||
<td>{{ row[col] }}</td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div id="{{ viz.token }}" style="display: none;">
|
||||
</div>
|
||||
<img src="{{ url_for("static", filename="loading.gif") }}" class="loading">
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% 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({
|
||||
{% macro viz_js(viz) %}
|
||||
{% if viz.args.get("async") != "true" %}
|
||||
<script>
|
||||
var url = "{{ viz.get_url(async="true", standalone="true", skip_libs="true")|safe }}";
|
||||
console.log(url);
|
||||
$("#{{ viz.token }}").load(url, function(){
|
||||
var table = $('table').DataTable({
|
||||
paging: false,
|
||||
searching: false,
|
||||
});
|
||||
$("#{{ viz.token }}").show();
|
||||
table.column('-1').order( 'desc' ).draw();
|
||||
$("#{{ viz.token }} img.loading").hide();
|
||||
});
|
||||
table.column('-1').order( 'desc' ).draw();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro viz_css(viz) %}
|
||||
{% endmacro %}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,34 @@ from datetime import datetime
|
|||
from sqlalchemy.types import TypeDecorator, TEXT
|
||||
import json
|
||||
import parsedatetime
|
||||
import functools
|
||||
|
||||
|
||||
class memoized(object):
|
||||
"""Decorator that caches a function's return value each time it is called.
|
||||
If called later with the same arguments, the cached value is returned, and
|
||||
not re-evaluated.
|
||||
"""
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
self.cache = {}
|
||||
def __call__(self, *args):
|
||||
try:
|
||||
return self.cache[args]
|
||||
except KeyError:
|
||||
value = self.func(*args)
|
||||
self.cache[args] = value
|
||||
return value
|
||||
except TypeError:
|
||||
# uncachable -- for instance, passing a list as an argument.
|
||||
# Better to not cache than to blow up entirely.
|
||||
return self.func(*args)
|
||||
def __repr__(self):
|
||||
"""Return the function's docstring."""
|
||||
return self.func.__doc__
|
||||
def __get__(self, obj, objtype):
|
||||
"""Support instance methods."""
|
||||
return functools.partial(self.__call__, obj)
|
||||
|
||||
|
||||
def parse_human_datetime(s):
|
||||
|
|
|
|||
|
|
@ -104,6 +104,33 @@ appbuilder.add_view(
|
|||
category_icon='fa-cogs',)
|
||||
|
||||
|
||||
class SliceModelView(ModelView, DeleteMixin):
|
||||
datamodel = SQLAInterface(models.Slice)
|
||||
list_columns = ['slice_link', 'viz_type', 'datasource', 'created_by']
|
||||
|
||||
appbuilder.add_view(
|
||||
SliceModelView,
|
||||
"Slices",
|
||||
icon="fa-bar-chart",
|
||||
category="",
|
||||
category_icon='',)
|
||||
|
||||
|
||||
class DashboardModelView(ModelView, DeleteMixin):
|
||||
datamodel = SQLAInterface(models.Dashboard)
|
||||
list_columns = ['dashboard_link', 'created_by']
|
||||
edit_columns = ['dashboard_title', 'slices', 'position_json']
|
||||
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']
|
||||
|
|
@ -159,7 +186,7 @@ class DatasourceModelView(ModelView, DeleteMixin):
|
|||
appbuilder.add_view(
|
||||
DatasourceModelView,
|
||||
"Druid Datasources",
|
||||
icon="fa-cube")
|
||||
icon="fa-cubes")
|
||||
|
||||
|
||||
@app.route('/health')
|
||||
|
|
@ -176,13 +203,14 @@ class Panoramix(BaseView):
|
|||
@has_access
|
||||
@expose("/table/<table_id>/")
|
||||
def table(self, table_id):
|
||||
|
||||
table = (
|
||||
db.session
|
||||
.query(models.Table)
|
||||
.filter_by(id=table_id)
|
||||
.first()
|
||||
)
|
||||
if not table:
|
||||
flash("The table seem to have been deleted", "alert")
|
||||
viz_type = request.args.get("viz_type")
|
||||
if not viz_type and table.default_endpoint:
|
||||
return redirect(table.default_endpoint)
|
||||
|
|
@ -190,19 +218,43 @@ class Panoramix(BaseView):
|
|||
viz_type = "table"
|
||||
obj = viz.viz_types[viz_type](
|
||||
table,
|
||||
form_data=request.args, view=self)
|
||||
if request.args.get("json"):
|
||||
form_data=request.args)
|
||||
if request.args.get("json") == "true":
|
||||
try:
|
||||
payload = obj.get_json()
|
||||
status=200
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
payload = str(e)
|
||||
status=500
|
||||
return Response(
|
||||
json.dumps(obj.get_query(), indent=4),
|
||||
status=200,
|
||||
payload,
|
||||
status=status,
|
||||
mimetype="application/json")
|
||||
return obj.check_and_render()
|
||||
else:
|
||||
return self.render_template("panoramix/viz.html", viz=obj)
|
||||
|
||||
@has_access
|
||||
@expose("/save_dash/<dashboard_id>/", methods=['GET', 'POST'])
|
||||
def save_dash(self, dashboard_id):
|
||||
data = json.loads(request.form.get('data'))
|
||||
slice_ids = [int(d['slice_id']) for d in data]
|
||||
print slice_ids
|
||||
session = db.session()
|
||||
Dash = models.Dashboard
|
||||
dash = session.query(Dash).filter_by(id=dashboard_id).first()
|
||||
dash.slices = [o for o in dash.slices if o.id in slice_ids]
|
||||
print dash.slices
|
||||
dash.position_json = json.dumps(data, indent=4)
|
||||
session.merge(dash)
|
||||
session.commit()
|
||||
session.close()
|
||||
return "SUCCESS"
|
||||
|
||||
@has_access
|
||||
@expose("/datasource/<datasource_name>/")
|
||||
def datasource(self, datasource_name):
|
||||
viz_type = request.args.get("viz_type")
|
||||
|
||||
datasource = (
|
||||
db.session
|
||||
.query(models.Datasource)
|
||||
|
|
@ -215,7 +267,7 @@ class Panoramix(BaseView):
|
|||
viz_type = "table"
|
||||
obj = viz.viz_types[viz_type](
|
||||
datasource,
|
||||
form_data=request.args, view=self)
|
||||
form_data=request.args)
|
||||
if request.args.get("json"):
|
||||
return Response(
|
||||
json.dumps(obj.get_query(), indent=4),
|
||||
|
|
@ -226,6 +278,55 @@ class Panoramix(BaseView):
|
|||
|
||||
return obj.check_and_render()
|
||||
|
||||
@has_access
|
||||
@expose("/save/")
|
||||
def save(self):
|
||||
session = db.session()
|
||||
d = request.args.to_dict(flat=False)
|
||||
as_list = ('metrics', 'groupby')
|
||||
for m in as_list:
|
||||
v = d.get(m)
|
||||
if v and not isinstance(d[m], list):
|
||||
d[m] = [d[m]]
|
||||
|
||||
table_id = druid_datasource_id = None
|
||||
datasource_type = request.args.get('datasource_type')
|
||||
if datasource_type == 'druid':
|
||||
druid_datasource_id = request.args.get('datasource_id')
|
||||
else:
|
||||
table_id = request.args.get('datasource_id')
|
||||
|
||||
obj = models.Slice(
|
||||
params=json.dumps(d, indent=4),
|
||||
viz_type=request.args.get('viz_type'),
|
||||
datasource_name=request.args.get('datasource_name'),
|
||||
druid_datasource_id=druid_datasource_id,
|
||||
table_id=table_id,
|
||||
datasource_type=datasource_type,
|
||||
slice_name=request.args.get('slice_name', 'junk'),
|
||||
)
|
||||
session.add(obj)
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
return "super!"
|
||||
|
||||
@has_access
|
||||
@expose("/dashboard/<id_>/")
|
||||
def dashboard(self, id_):
|
||||
session = db.session()
|
||||
dashboard = (
|
||||
session
|
||||
.query(models.Dashboard)
|
||||
.filter(models.Dashboard.id == id_)
|
||||
.first()
|
||||
)
|
||||
pos_dict = {
|
||||
int(o['slice_id']):o for o in json.loads(dashboard.position_json)}
|
||||
return self.render_template(
|
||||
"panoramix/dashboard.html", dashboard=dashboard,
|
||||
pos_dict=pos_dict)
|
||||
|
||||
@has_access
|
||||
@expose("/refresh_datasources/")
|
||||
def refresh_datasources(self):
|
||||
|
|
|
|||
229
panoramix/viz.py
229
panoramix/viz.py
|
|
@ -1,55 +1,83 @@
|
|||
from datetime import datetime
|
||||
from flask import flash, request
|
||||
import pandas as pd
|
||||
from collections import OrderedDict
|
||||
import config
|
||||
import logging
|
||||
import numpy as np
|
||||
from datetime import datetime
|
||||
from urllib import urlencode
|
||||
import uuid
|
||||
|
||||
from panoramix import utils
|
||||
from flask import flash
|
||||
from werkzeug.datastructures import MultiDict
|
||||
from werkzeug.urls import Href
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from panoramix import utils, config
|
||||
from panoramix.highchart import Highchart, HighchartBubble
|
||||
from panoramix.forms import form_factory
|
||||
|
||||
CHART_ARGS = {
|
||||
'height': 700,
|
||||
'title': None,
|
||||
'target_div': 'chart',
|
||||
}
|
||||
|
||||
|
||||
class BaseViz(object):
|
||||
verbose_name = "Base Viz"
|
||||
template = "panoramix/datasource.html"
|
||||
template = None
|
||||
hidden_fields = []
|
||||
form_fields = [
|
||||
'viz_type', 'metrics', 'groupby', 'granularity',
|
||||
('since', 'until')]
|
||||
js_files = []
|
||||
css_files = []
|
||||
|
||||
def __init__(self, datasource, form_data, view):
|
||||
def __init__(self, datasource, form_data):
|
||||
self.datasource = datasource
|
||||
self.form_class = self.form_class()
|
||||
self.view = view
|
||||
if isinstance(form_data, MultiDict):
|
||||
self.args = form_data.to_dict(flat=True)
|
||||
else:
|
||||
self.args = form_data
|
||||
self.form_data = form_data
|
||||
self.metrics = form_data.getlist('metrics') or ['count']
|
||||
self.groupby = form_data.getlist('groupby') or []
|
||||
self.token = self.args.get('token', 'token_' + uuid.uuid4().hex[:8])
|
||||
|
||||
as_list = ('metrics', 'groupby')
|
||||
d = self.args
|
||||
for m in as_list:
|
||||
if m in d and d[m] and not isinstance(d[m], list):
|
||||
d[m] = [d[m]]
|
||||
self.metrics = self.args.get('metrics') or ['count']
|
||||
self.groupby = self.args.get('groupby') or []
|
||||
|
||||
def get_url(self, **kwargs):
|
||||
d = self.args.copy()
|
||||
d.update(kwargs)
|
||||
href = Href('/panoramix/table/2/')
|
||||
return href(d)
|
||||
|
||||
def get_df(self):
|
||||
self.error_msg = ""
|
||||
self.results = None
|
||||
try:
|
||||
self.results = self.bake_query()
|
||||
self.df = self.results.df
|
||||
if self.df is not None:
|
||||
if 'timestamp' in self.df.columns:
|
||||
self.df.timestamp = pd.to_datetime(self.df.timestamp)
|
||||
|
||||
#try:
|
||||
self.results = self.bake_query()
|
||||
df = self.results.df
|
||||
if df is not None:
|
||||
if 'timestamp' in df.columns:
|
||||
df.timestamp = pd.to_datetime(df.timestamp)
|
||||
return df
|
||||
'''
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
self.error_msg = str(e)
|
||||
'''
|
||||
|
||||
@property
|
||||
def form(self):
|
||||
return self.form_class(self.form_data)
|
||||
|
||||
@property
|
||||
def form_class(self):
|
||||
return form_factory(self)
|
||||
|
||||
def query_filters(self):
|
||||
args = self.form_data
|
||||
args = self.args
|
||||
# Building filters
|
||||
filters = []
|
||||
for i in range(1, 10):
|
||||
|
|
@ -67,9 +95,9 @@ class BaseViz(object):
|
|||
"""
|
||||
Building a query object
|
||||
"""
|
||||
args = self.form_data
|
||||
groupby = args.getlist("groupby") or []
|
||||
metrics = args.getlist("metrics") or ['count']
|
||||
args = self.args
|
||||
groupby = args.get("groupby") or []
|
||||
metrics = args.get("metrics") or ['count']
|
||||
granularity = args.get("granularity", "1 day")
|
||||
if granularity != "all":
|
||||
granularity = utils.parse_human_timedelta(
|
||||
|
|
@ -85,7 +113,7 @@ class BaseViz(object):
|
|||
to_dttm = utils.parse_human_datetime(until)
|
||||
if from_dttm >= to_dttm:
|
||||
flash("The date range doesn't seem right.", "danger")
|
||||
from_dttm = to_dttm # Making them identicial to not raise
|
||||
from_dttm = to_dttm # Making them identical to not raise
|
||||
|
||||
# extras are used to query elements specific to a datasource type
|
||||
# for instance the extra where clause that applies only to Tables
|
||||
|
|
@ -106,36 +134,13 @@ class BaseViz(object):
|
|||
}
|
||||
return d
|
||||
|
||||
def render_no_data(self):
|
||||
self.template = "panoramix/no_data.html"
|
||||
return BaseViz.render(self)
|
||||
|
||||
def check_and_render(self, *args, **kwards):
|
||||
if (
|
||||
hasattr(self, 'df') and
|
||||
self.df is not None and
|
||||
len(self.df) == config.ROW_LIMIT):
|
||||
self.warning_msg = (
|
||||
"Doh! The system limit of {} rows was reached, "
|
||||
"showing partial results.").format(config.ROW_LIMIT)
|
||||
|
||||
if self.error_msg:
|
||||
return BaseViz.render(self, error_msg=self.error_msg)
|
||||
else:
|
||||
return self.render(*args, **kwards)
|
||||
|
||||
def render(self, *args, **kwargs):
|
||||
form = self.form_class(self.form_data)
|
||||
return self.view.render_template(
|
||||
self.template, form=form, viz=self, datasource=self.datasource,
|
||||
results=self.results,
|
||||
*args, **kwargs)
|
||||
|
||||
|
||||
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()
|
||||
|
|
@ -143,27 +148,25 @@ class TableViz(BaseViz):
|
|||
d['timeseries_limit'] = None
|
||||
return d
|
||||
|
||||
def render(self):
|
||||
df = self.df
|
||||
if df is None or df.empty:
|
||||
return super(TableViz, self).render(error_msg="No data.")
|
||||
else:
|
||||
if (
|
||||
self.form_data.get("granularity") == "all" and
|
||||
'timestamp' in df):
|
||||
del df['timestamp']
|
||||
for m in self.metrics:
|
||||
df[m + '__perc'] = np.rint((df[m] / np.max(df[m])) * 100)
|
||||
return super(TableViz, self).render(df=df)
|
||||
def get_df(self):
|
||||
df = super(TableViz, self).get_df()
|
||||
if (
|
||||
self.form_data.get("granularity") == "all" and
|
||||
'timestamp' in df):
|
||||
del df['timestamp']
|
||||
for m in self.metrics:
|
||||
df[m + '__perc'] = np.rint((df[m] / np.max(df[m])) * 100)
|
||||
return df
|
||||
|
||||
|
||||
class HighchartsViz(BaseViz):
|
||||
verbose_name = "Base Highcharts Viz"
|
||||
template = 'panoramix/viz_highcharts.html'
|
||||
chart_kind = 'line'
|
||||
chart_call = "Chart"
|
||||
stacked = False
|
||||
chart_type = 'not_stock'
|
||||
compare = False
|
||||
js_files = ['highstock.js']
|
||||
|
||||
|
||||
class BubbleViz(HighchartsViz):
|
||||
|
|
@ -173,19 +176,21 @@ class BubbleViz(HighchartsViz):
|
|||
form_fields = [
|
||||
'viz_type', 'since', 'until',
|
||||
'series', 'entity', 'x', 'y', 'size', 'limit']
|
||||
js_files = ['highstock.js', 'highcharts-more.js']
|
||||
|
||||
def query_obj(self):
|
||||
args = self.form_data
|
||||
d = super(BubbleViz, self).query_obj()
|
||||
d['granularity'] = 'all'
|
||||
d['groupby'] = list({
|
||||
request.args.get('series'),
|
||||
request.args.get('entity')
|
||||
})
|
||||
self.x_metric = request.args.get('x')
|
||||
self.y_metric = request.args.get('y')
|
||||
self.z_metric = request.args.get('size')
|
||||
self.entity = request.args.get('entity')
|
||||
self.series = request.args.get('series')
|
||||
args.get('series'),
|
||||
args.get('entity')
|
||||
})
|
||||
self.x_metric = args.get('x')
|
||||
self.y_metric = args.get('y')
|
||||
self.z_metric = args.get('size')
|
||||
self.entity = args.get('entity')
|
||||
self.series = args.get('series')
|
||||
d['metrics'] = [
|
||||
self.z_metric,
|
||||
self.x_metric,
|
||||
|
|
@ -195,22 +200,28 @@ class BubbleViz(HighchartsViz):
|
|||
raise Exception("Pick a metric for x, y and size")
|
||||
return d
|
||||
|
||||
def render(self):
|
||||
df = self.df.fillna(0)
|
||||
def get_df(self):
|
||||
df = super(BubbleViz, self).get_df()
|
||||
df = df.fillna(0)
|
||||
df['x'] = df[[self.x_metric]]
|
||||
df['y'] = df[[self.y_metric]]
|
||||
df['z'] = df[[self.z_metric]]
|
||||
df['name'] = df[[self.entity]]
|
||||
df['group'] = df[[self.series]]
|
||||
return df
|
||||
|
||||
def get_json(self):
|
||||
df = self.get_df()
|
||||
chart = HighchartBubble(df)
|
||||
return super(BubbleViz, self).render(chart_js=chart.javascript_cmd)
|
||||
return chart.json
|
||||
|
||||
|
||||
class TimeSeriesViz(HighchartsViz):
|
||||
verbose_name = "Time Series - Line Chart"
|
||||
chart_type = "spline"
|
||||
stockchart = True
|
||||
chart_call = "StockChart"
|
||||
sort_legend_y = True
|
||||
js_files = ['highstock.js', 'highcharts-more.js']
|
||||
form_fields = [
|
||||
'viz_type',
|
||||
'granularity', ('since', 'until'),
|
||||
|
|
@ -219,21 +230,17 @@ class TimeSeriesViz(HighchartsViz):
|
|||
('rolling_type', 'rolling_periods'),
|
||||
]
|
||||
|
||||
def render(self):
|
||||
if request.args.get("granularity") == "all":
|
||||
self.error_msg = (
|
||||
"You have to select a time granularity for this view")
|
||||
return super(TimeSeriesViz, self).render(error_msg=self.error_msg)
|
||||
|
||||
def get_df(self):
|
||||
args = self.args
|
||||
df = super(TimeSeriesViz, self).get_df()
|
||||
metrics = self.metrics
|
||||
df = self.df
|
||||
df = df.pivot_table(
|
||||
index="timestamp",
|
||||
columns=self.groupby,
|
||||
values=metrics,)
|
||||
|
||||
rolling_periods = request.args.get("rolling_periods")
|
||||
rolling_type = request.args.get("rolling_type")
|
||||
rolling_periods = args.get("rolling_periods")
|
||||
rolling_type = args.get("rolling_type")
|
||||
if rolling_periods and rolling_type:
|
||||
if rolling_type == 'mean':
|
||||
df = pd.rolling_mean(df, int(rolling_periods))
|
||||
|
|
@ -241,22 +248,18 @@ class TimeSeriesViz(HighchartsViz):
|
|||
df = pd.rolling_std(df, int(rolling_periods))
|
||||
elif rolling_type == 'sum':
|
||||
df = pd.rolling_sum(df, int(rolling_periods))
|
||||
return df
|
||||
|
||||
def get_json(self):
|
||||
df = self.get_df()
|
||||
chart = Highchart(
|
||||
df,
|
||||
compare=self.compare,
|
||||
chart_type=self.chart_type,
|
||||
stacked=self.stacked,
|
||||
stockchart=self.stockchart,
|
||||
sort_legend_y=self.sort_legend_y,
|
||||
**CHART_ARGS)
|
||||
return super(TimeSeriesViz, self).render(chart_js=chart.javascript_cmd)
|
||||
|
||||
def bake_query(self):
|
||||
"""
|
||||
Doing a 2 phase query where we limit the number of series.
|
||||
"""
|
||||
return self.datasource.query(**self.query_obj())
|
||||
return chart.json
|
||||
|
||||
|
||||
class TimeSeriesCompareViz(TimeSeriesViz):
|
||||
|
|
@ -286,32 +289,12 @@ class TimeSeriesStackedBarViz(TimeSeriesViz):
|
|||
stacked = True
|
||||
|
||||
|
||||
class DistributionBarViz(HighchartsViz):
|
||||
verbose_name = "Distribution - Bar Chart"
|
||||
chart_type = "column"
|
||||
form_fields = BaseViz.form_fields + ['limit']
|
||||
|
||||
def query_obj(self):
|
||||
d = super(DistributionBarViz, self).query_obj()
|
||||
d['granularity'] = "all"
|
||||
d['is_timeseries'] = False
|
||||
return d
|
||||
|
||||
def render(self):
|
||||
df = self.df
|
||||
df = df.pivot_table(
|
||||
index=self.groupby,
|
||||
values=self.metrics)
|
||||
df = df.sort(self.metrics[0], ascending=False)
|
||||
chart = Highchart(
|
||||
df, chart_type=self.chart_type, **CHART_ARGS)
|
||||
return super(DistributionBarViz, self).render(
|
||||
chart_js=chart.javascript_cmd)
|
||||
|
||||
|
||||
class DistributionPieViz(HighchartsViz):
|
||||
verbose_name = "Distribution - Pie Chart"
|
||||
chart_type = "pie"
|
||||
js_files = ['highstock.js']
|
||||
form_fields = BaseViz.form_fields + ['limit']
|
||||
|
||||
def query_obj(self):
|
||||
|
|
@ -320,16 +303,26 @@ class DistributionPieViz(HighchartsViz):
|
|||
d['is_timeseries'] = False
|
||||
return d
|
||||
|
||||
def render(self):
|
||||
df = self.df
|
||||
def get_df(self):
|
||||
df = super(DistributionPieViz, self).get_df()
|
||||
df = df.pivot_table(
|
||||
index=self.groupby,
|
||||
values=[self.metrics[0]])
|
||||
df = df.sort(self.metrics[0], ascending=False)
|
||||
return df
|
||||
|
||||
def get_json(self):
|
||||
df = self.get_df()
|
||||
chart = Highchart(
|
||||
df, chart_type=self.chart_type, **CHART_ARGS)
|
||||
return super(DistributionPieViz, self).render(
|
||||
chart_js=chart.javascript_cmd)
|
||||
self.chart_js = chart.javascript_cmd
|
||||
return chart.json
|
||||
|
||||
|
||||
class DistributionBarViz(DistributionPieViz):
|
||||
verbose_name = "Distribution - Bar Chart"
|
||||
chart_type = "column"
|
||||
|
||||
|
||||
viz_types = OrderedDict([
|
||||
['table', TableViz],
|
||||
|
|
|
|||
3
setup.py
3
setup.py
|
|
@ -1,6 +1,6 @@
|
|||
from setuptools import setup, find_packages
|
||||
|
||||
version = '0.2.1'
|
||||
version = '0.3.0'
|
||||
|
||||
setup(
|
||||
name='panoramix',
|
||||
|
|
@ -17,6 +17,7 @@ setup(
|
|||
'flask-appbuilder>=1.4.5',
|
||||
'flask-login==0.2.11',
|
||||
'flask-migrate>=1.5.1',
|
||||
'flask-login==0.2.11',
|
||||
'gunicorn>=19.3.0',
|
||||
'pandas>=0.16.2',
|
||||
'pydruid>=0.2.2',
|
||||
|
|
|
|||
Loading…
Reference in New Issue