Integrated the admin

This commit is contained in:
Maxime 2015-07-14 20:26:35 +00:00
parent 66dca37c9c
commit 3bce904454
17 changed files with 573 additions and 328 deletions

86
base.html Normal file
View File

@ -0,0 +1,86 @@
{% import 'admin/layout.html' as layout with context -%}
{% import 'admin/static.html' as admin_static with context %}
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}{% if admin_view.category %}{{ admin_view.category }} - {% endif %}{{ admin_view.name }} - {{ admin_view.admin.name }}{% endblock %}</title>
{% block head_meta %}
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
{% endblock %}
{% block head_css %}
<link href="{{ admin_static.url(filename='bootstrap/bootstrap3/css/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='bootstrap-theme.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='main.css') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='admin/css/bootstrap3/admin.css') }}" rel="stylesheet">
<link rel="stylesheet" href="{{url_for('static', filename='select2-bootstrap.css')}}">
<link rel="stylesheet" href="{{url_for('static', filename='select2.min.css')}}">
{% endblock %}
{% block head %}
{% endblock %}
{% block head_tail %}
{% endblock %}
</head>
<body>
{% block page_body %}
<div class="container">
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#admin-navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
{% block brand %}
<a class="navbar-brand" href="#">{{ admin_view.admin.name }}</a>
{% endblock %}
</div>
<!-- navbar content -->
<div class="collapse navbar-collapse" id="admin-navbar-collapse">
{% block main_menu %}
<ul class="nav navbar-nav">
{{ layout.menu() }}
</ul>
{% endblock %}
{% block menu_links %}
<ul class="nav navbar-right">
{{ layout.menu_links() }}
</ul>
{% endblock %}
{% block access_control %}
{% endblock %}
<ul class="nav navbar-nav navbar-right">
</ul>
</div>
</div>
</nav>
{% block messages %}
{{ layout.messages() }}
{% endblock %}
{% set render_ctx = h.resolve_ctx() %}
{% block body %}{% endblock %}
</div>
{% endblock %}
{% block tail_js %}
<script src="{{ admin_static.url(filename='vendor/jquery-2.1.1.min.js') }}" type="text/javascript"></script>
<script src="{{ admin_static.url(filename='bootstrap/bootstrap3/js/bootstrap.min.js') }}" type="text/javascript"></script>
<script src="{{ admin_static.url(filename='vendor/moment-2.8.4.min.js') }}" type="text/javascript"></script>
<script src="{{ admin_static.url(filename='vendor/select2/select2.min.js') }}" type="text/javascript"></script>
{% endblock %}
{% block tail %}
{% endblock %}
</body>
</html>

View File

@ -1,40 +1,78 @@
from dateutil.parser import parse
from datetime import timedelta
from flask import Flask, request, Blueprint
from panoramix import settings, viz
from flask import Flask, request, Blueprint, url_for, Markup
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.admin import Admin, BaseView, expose, AdminIndexView
from panoramix import settings, viz, models
from flask_bootstrap import Bootstrap
from wtforms import Form, SelectMultipleField, SelectField, TextField
from wtforms.fields import Field
import pandas as pd
from flask_admin.contrib import sqla
pd.set_option('display.max_colwidth', -1)
since_l = {
'1hour': timedelta(hours=1),
'1day': timedelta(days=1),
'7days': timedelta(days=7),
'28days': timedelta(days=28),
'all': timedelta(days=365*100)
}
client = settings.get_pydruid_client()
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
class DruidDataSource(object):
def __init__(self, name):
self.name = name
self.cols = self.latest_metadata()
self.col_names = sorted([col for col in self.cols.keys()])
self.col_names = sorted([
col for col in self.cols.keys()
if not col.startswith("_") and col not in self.metrics])
def latest_metadata(self):
max_time = client.time_boundary(
datasource=self.name)[0]['result']['maxTime']
results = client.time_boundary(datasource=self.name)
max_time = results[0]['result']['maxTime']
max_time = parse(max_time)
intervals = (max_time - timedelta(seconds=1)).isoformat() + '/'
intervals += max_time.isoformat()
return client.segment_metadata(
intervals += (max_time + timedelta(seconds=1)).isoformat()
segment_metadata = client.segment_metadata(
datasource=self.name,
intervals=intervals)[-1]['columns']
intervals=intervals)
return segment_metadata[-1]['columns']
@property
def metrics(self):
return [
k for k, v in self.cols.items()
if v['type'] != 'STRING' and not k.startswith('_')]
def sync_to_db(self):
DS = Datasource
datasource = DS.query.filter_by(datasource_name=self.name).first()
if not datasource:
db.session.add(DS(datasource_name=self.name))
for col in self.cols:
col_obj = Column.query.filter_by(datasource_name=self.name, column_name=col).first()
datatype = self.cols[col]['type']
if not col_obj:
col_obj = Column(datasource_name=self.name, column_name=col)
db.session.add(col_obj)
if datatype == "STRING":
col_obj.groupby = True
if col_obj:
col_obj.type = self.cols[col]['type']
db.session.commit()
def form_factory(datasource, form_args=None):
grain = ['all', 'none', 'minute', 'hour', 'day']
@ -50,48 +88,154 @@ def form_factory(datasource, form_args=None):
except:
pass
class QueryForm(Form):
class QueryForm(OmgWtForm):
viz_type = SelectField(
'Viz',
choices=[(k, v.verbose_name) for k, v in viz.viz_types.items()])
metric = SelectField(
'Metric', choices=[(m, m) for m in datasource.metrics])
groupby = SelectMultipleField(
'Group by', choices=[(m, m) for m in datasource.col_names])
granularity = SelectField(
'Granularity', choices=[(g, g) for g in grain])
since = SelectField(
'Since', choices=[(s, s) for s in since_l.keys()])
'Since', choices=[(s, s) for s in settings.since_l.keys()],
default="all")
limit = SelectField(
'Limit', choices=[(s, s) for s in limits])
flt_col_1 = SelectField(
'Filter 1', choices=[(m, m) for m in datasource.col_names])
flt_op_1 = SelectField(
'Filter 1', choices=[(m, m) for m in ['==', '!=', 'in',]])
flt_eq_1 = TextField("Super")
for i in range(10):
setattr(QueryForm, 'flt_col_' + str(i), SelectField(
'Filter 1', choices=[(m, m) for m in datasource.col_names]))
setattr(QueryForm, 'flt_op_' + str(i), SelectField(
'Filter 1', choices=[(m, m) for m in ['==', '!=', 'in',]]))
setattr(QueryForm, 'flt_eq_' + str(i), TextField("Super"))
return QueryForm
"""
bp = Blueprint(
'panoramix', __name__,
template_folder='templates',
static_folder='static')
"""
@bp.route("/datasource/<name>/")
def datasource(name):
viz_type = request.args.get("viz_type", "table")
datasource = DruidDataSource(name)
obj = viz.viz_types[viz_type](
datasource,
form_class=form_factory(datasource, request.args),
form_data=request.args)
return obj.render()
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = settings.SQLALCHEMY_DATABASE_URI
db = SQLAlchemy(app)
app.secret_key = "monkeys"
#app.register_blueprint(bp, url_prefix='/panoramix')
Bootstrap(app)
admin = Admin(
app, name = "Panoramix",
template_mode='bootstrap3')
class Datasource(db.Model):
__tablename__ = 'datasources'
id = db.Column(db.Integer, primary_key=True)
datasource_name = db.Column(db.String(256), unique=True)
is_featured = db.Column(db.Boolean, default=False)
is_hidden = db.Column(db.Boolean, default=False)
description = db.Column(db.Text)
created_dttm = db.Column(db.DateTime, default=db.func.now())
class Column(db.Model):
__tablename__ = 'columns'
id = db.Column(db.Integer, primary_key=True)
datasource_name = db.Column(
db.String(256),
db.ForeignKey('datasources.datasource_name'))
column_name = db.Column(db.String(256))
is_active = db.Column(db.Boolean, default=True)
type = db.Column(db.String(32))
groupby = db.Column(db.Boolean, default=False)
count_distinct = db.Column(db.Boolean, default=False)
sum = db.Column(db.Boolean, default=False)
max = db.Column(db.Boolean, default=False)
min = db.Column(db.Boolean, default=False)
datasource = db.relationship('Datasource',
backref=db.backref('columns', lazy='dynamic'))
def __repr__(self):
return self.column_name
class JsUdf(db.Model):
__tablename__ = 'udfs'
id = db.Column(db.Integer, primary_key=True)
datasource_name = db.Column(
db.String(256),
db.ForeignKey('datasources.datasource_name'))
udf_name = db.Column(db.String(256))
column_list = db.Column(db.String(1024))
code = db.Column(db.Text)
datasource = db.relationship('Datasource',
backref=db.backref('udfs', lazy='dynamic'))
def datasource_link(v, c, m, p):
url = '/admin/datasourceview/datasource/{}/'.format(m.datasource_name)
return Markup('<a href="{url}">{m.datasource_name}</a>'.format(**locals()))
class DatasourceAdmin(sqla.ModelView):
inline_models = (Column, JsUdf,)
column_formatters = dict(datasource_name=datasource_link)
class DatasourceView(BaseView):
@expose('/')
def index(self):
return ""
@expose("/datasource/<datasource_name>/")
def datasource(self, datasource_name):
viz_type = request.args.get("viz_type", "table")
datasource = DruidDataSource(datasource_name)
obj = viz.viz_types[viz_type](
datasource,
form_class=form_factory(datasource, request.args),
form_data=request.args,
admin_view=self)
if obj.df is None or obj.df.empty:
return obj.render_no_data()
return obj.render()
@expose("/datasources/")
def datasources():
import requests
import json
endpoint = (
"http://{COORDINATOR_HOST}:{COORDINATOR_PORT}/"
"{COORDINATOR_BASE_ENDPOINT}/datasources"
).format(**settings.__dict__)
datasources = json.loads(requests.get(endpoint).text)
for datasource in datasources:
ds = DruidDataSource(datasource)
ds.sync_to_db()
return json.dumps(datasources, indent=4)
@expose("/datasource_metadata/<name>/")
def datasource_metadata(name):
import requests
import json
endpoint = (
"http://{COORDINATOR_HOST}:{COORDINATOR_PORT}/"
"{COORDINATOR_BASE_ENDPOINT}/datasource"
).format(**settings.__dict__)
return str(datasources)
admin.add_view(DatasourceView(name="Datasource"))
if __name__ == '__main__':
app = Flask(__name__)
app.secret_key = "monkeys"
app.register_blueprint(bp, url_prefix='/panoramix')
Bootstrap(app)
db.create_all()
admin.add_view(DatasourceAdmin(Datasource, db.session, name="Datasources"))
app.debug = True
app.run(host='0.0.0.0', port=settings.FLASK_APP_PORT)

0
panoramix/models.py Normal file
View File

View File

@ -1,11 +1,26 @@
from datetime import timedelta
FLASK_APP_PORT = 8088
ROW_LIMIT = 10000
SQLALCHEMY_DATABASE_URI = "sqlite:////tmp/panoramix.db"
DRUID_HOST = '10.181.47.80'
DRUID_PORT = 8088
DRUID_PORT = 8080
DRUID_BASE_ENDPOINT = 'druid/v2'
COORDINATOR_HOST = '10.168.176.249'
COORDINATOR_PORT = '8080'
COORDINATOR_BASE_ENDPOINT = 'druid/coordinator/v1'
since_l = {
'1hour': timedelta(hours=1),
'1day': timedelta(days=1),
'7days': timedelta(days=7),
'28days': timedelta(days=28),
'all': timedelta(days=365*100)
}
def get_pydruid_client():
from pydruid import client
return client.PyDruid(

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -19,53 +19,10 @@ button.btn {
border: 1px solid black;
}
div.rich_doc {
padding: 5px 10px;
border: 1px solid #dddddd;
background: white;
border-radius: 4px;
}
span.status_square {
width:10px;
height:10px;
border:1px solid grey;
display:inline-block;
padding-left: 0px;
cursor: pointer;
}
div.squares{
float:right;
font-size: 1;
}
div.task_row{
}
span.success{
background-color:green;
}
span.up_for_retry{
background-color:yellow;
}
span.started{
background-color:lime;
}
span.error{
background-color:red;
}
span.queued{
background-color:gray;
}
.tooltip-inner {
text-align:left !important;
font-size: 12px;
}
input#execution_date {
margin-bottom: 0px;
}
table.highlighttable{
width: 100%;
table-layout:fixed;
}
div.linenodiv {
padding-right: 1px !important;
}
@ -97,18 +54,26 @@ input, select {
font-family: monospace;
}
#sql {
border: 1px solid #CCC;
border-radius: 5px;
table.dataframe {
font-size: 12px;
}
.ace_editor div {
font: inherit!important
table.dataframe tbody tr td {
padding: 2px;
}
#ace_container {
margin: 10px 0px;
table thead th {
background-color: #F3F3F3;
}
#sql_ace {
visibility: hidden;
table.dataframe.dataTable thead > tr > th {
padding: 10px 20px 10px 10px;
}
table.dataTable.dataframe thead .sorting {
background: url('sort_both.png') no-repeat center right
}
table.dataTable.dataframe thead .sorting_desc {
background: url('sort_desc.png') no-repeat center right
}
table.dataTable.dataframe thead .sorting_asc {
background: url('sort_asc.png') no-repeat center right
}
.no-wrap {
white-space: nowrap;
@ -123,85 +88,3 @@ body div.panel {
.blur {
filter:url(#blur-effect-1);
}
div.legend_item {
-moz-border-radius: 5px/5px;
-webkit-border-radius: 5px 5px;
border-radius: 5px/5px;
float:right;
margin: 0px 10px 0px 0px;
padding:0px 5px;
border:solid 2px grey;
font-size: 12px;
}
div.legend_circle{
-moz-border-radius: 10px/10px;
-webkit-border-radius: 10px 10px;
border-radius: 10px/10px;
width:15px;
height:15px;
border:1px solid grey;
float:left;
margin-top: 2px;
margin-right: 0px;
}
div.square {
width:12px;
height:12px;
float: right;
margin-top: 2px;
border:1px solid black;
}
.btn:active, .btn.active {
box-shadow: inset 0 6px 6px rgba(0, 0, 0, 0.4);
}
.hll { background-color: #ffffcc }
.c { color: #408080; font-style: italic } /* Comment */
.err { border: 1px solid #FF0000 } /* Error */
.k { color: #008000; font-weight: bold } /* Keyword */
.o { color: #666666 } /* Operator */
.cm { color: #408080; font-style: italic } /* Comment.Multiline */
.cp { color: #BC7A00 } /* Comment.Preproc */
.c1 { color: #408080; font-style: italic } /* Comment.Single */
.cs { color: #408080; font-style: italic } /* Comment.Special */
.gd { color: #A00000 } /* Generic.Deleted */
.ge { font-style: italic } /* Generic.Emph */
.gr { color: #FF0000 } /* Generic.Error */
.gh { color: #000080; font-weight: bold } /* Generic.Heading */
.gi { color: #00A000 } /* Generic.Inserted */
.go { color: #888888 } /* Generic.Output */
.gp { color: #000080; font-weight: bold } /* Generic.Prompt */
.gs { font-weight: bold } /* Generic.Strong */
.gu { color: #800080; font-weight: bold } /* Generic.Subheading */
.gt { color: #0044DD } /* Generic.Traceback */
.kc { color: #008000; font-weight: bold } /* Keyword.Constant */
.kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
.kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
.kp { color: #008000 } /* Keyword.Pseudo */
.kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
.kt { color: #B00040 } /* Keyword.Type */
.m { color: #666666 } /* Literal.Number */
.s { color: #BA2121 } /* Literal.String */
.na { color: #7D9029 } /* Name.Attribute */
.nb { color: #008000 } /* Name.Builtin */
.nc { color: #0000FF; font-weight: bold } /* Name.Class */
.no { color: #880000 } /* Name.Constant */
.nd { color: #AA22FF } /* Name.Decorator */
.ni { color: #999999; font-weight: bold } /* Name.Entity */
.ne { color: #D2413A; font-weight: bold } /* Name.Exception */
.nf { color: #0000FF } /* Name.Function */
.nl { color: #A0A000 } /* Name.Label */
.nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
.nt { color: #008000; font-weight: bold } /* Name.Tag */
.nv { color: #19177C } /* Name.Variable */
.ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
.w { color: #bbbbbb } /* Text.Whitespace */
.mb { color: #666666 } /* Literal.Number.Bin */
.mf { color: #666666 } /* Literal.Number.Float */
.mh { color: #666666 } /* Literal.Number.Hex */
.mi { color: #666666 } /* Literal.Number.Integer */
.mo { color: #666666 } /* Literal.Number.Oct */
.sb { color: #BA2121 } /* Literal.String.Backtick */
.sc { color: #BA2121 } /* Literal.String.Char */
.sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
.s2 { color: #BA2121 } /* Literal.String.Double */

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -0,0 +1,2 @@
{% extends "admin/base.html" %}

View File

@ -1,64 +1,2 @@
{% extends "bootstrap/base.html" %}
{% extends "index.html" %}
{% block title %}Panoramix - A Druid UI{% endblock %}
{% block html_attribs %} lang="en"{% endblock %}
{% block styles %}
{{super()}}
<link rel="stylesheet" href="{{url_for('.static', filename='bootstrap-theme.css')}}">
<link rel="stylesheet" href="{{url_for('.static', filename='main.css')}}">
<link rel="stylesheet" href="{{url_for('.static', filename='select2.min.css')}}">
<link rel="stylesheet" href="{{url_for('.static', filename='select2-bootstrap.css')}}">
{% endblock %}
{% block navbar %}
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#" class="pull-left">
Panoramix
</a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active"><a href="#">Link <span class="sr-only">(current)</span></a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="#">Action</a></li>
<li><a href="#">Another action</a></li>
<li><a href="#">Something else here</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">Separated link</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">One more separated link</a></li>
</ul>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="{{url_for('.static', filename='select2.min.js')}}"></script>
<script>
$( document ).ready(function() {
$(".select2").select2();
$(".select2_tags").select2({tags: true});
});
</script>
{% endblock %}

View File

@ -1,44 +1,89 @@
{% extends "panoramix/base.html" %}
{% block content %}
{% block styles %}
{{super()}}
<style>
form .row {
margin-left: 0;
margin-right: 0;
}
form .col {
padding-right:0px;
padding-left:0px;
}
</style>
{% endblock %}
{% block body %}
<div class="container">
<div class="col-md-3">
<h3>{{ datasource }}</h3>
<form method="GET">
<div>{{ form.viz_type.label }}: {{ form.viz_type(class_="form-control select2") }}</div>
<div>{{ form.granularity.label }}: {{ form.granularity(class_="form-control select2") }}</div>
<div>{{ form.since.label }}: {{ form.since(class_="form-control select2") }}</div>
<div>{{ form.groupby.label }}: {{ form.groupby(class_="form-control select2") }}</div>
<div>{{ form.limit.label }}: {{ form.limit(class_="form-control select2_tags") }}</div>
<hr>
<h4>Filters</h4>
<div>
<span style="width: 100px;">{{ form.flt_col_1(class_="form-control select2") }}</span>
<span>{{ form.flt_op_1(class_="form-control select2 input-sm") }}</span>
<span>{{ form.flt_eq_1(class_="form-control") }}</span>
<div class="col-md-3">
<h3>
{{ datasource.name }}
<a href="/admin/datasource/edit/?id={{ datasource.id }}"><span class="glyphicon glyphicon-edit"></span></a>
</h3>
<hr>
<form method="GET">
<div>{{ form.viz_type.label }}: {{ form.viz_type(class_="form-control") }}</div>
<div>{{ form.metric.label }}: {{ form.metric(class_="form-control select2") }}</div>
<div>{{ form.granularity.label }}: {{ form.granularity(class_="form-control") }}</div>
<div>{{ form.since.label }}: {{ form.since(class_="form-control") }}</div>
<div>{{ form.groupby.label }}: {{ form.groupby(class_="form-control") }}</div>
<div>{{ form.limit.label }}: {{ form.limit(class_="form-control") }}</div>
<hr>
<h4>Filters</h4>
<div id="filters">
{% for i in range(10) %}
<div id="flt{{ i }}" class="{{ "hidden" if i != 1 }}">
<div class="row">
<span class="" style="width: 100px;">{{ form['flt_col_' ~ i](class_="form-control select2 inc") }}</span>
</div>
<hr>
<input type="submit" class="btn btn-primary" value="Druidify!">
<hr>
<img src="{{ url_for("static", filename="tux_panoramix.png") }}" width=250>
</form><br>
</div>
<div class="row">
<span class="col col-md-3">{{ form['flt_op_' ~ i](class_="form-control select2 input-sm inc") }}</span>
<span class="col col-md-7">{{ form['flt_eq_' ~ i](class_="form-control inc") }}</span>
<button type="col col-md-2" class="btn btn-sm" aria-label="Delete filter">
<span class="glyphicon glyphicon-minus" aria-hidden="true"></span>
</button>
</div>
<hr/>
</div>
{% endfor %}
</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>
<input type="submit" class="btn btn-primary" value="Druidify!">
<hr style="margin-bottom: 0px;">
<img src="{{ url_for("static", filename="panoramix.png") }}" width=250>
</form><br>
</div>
<div class="col-md-9">
<h3>{{ verbose_viz_type }}</h3>
{% block viz %}
{% endblock %}
<div class="col-md-9">
<h3>{{ viz.verbose_name }}</h3>
<hr/>
{% block viz %}
{% endblock %}
{% if debug %}
<h3>Results</h3>
<pre>
{{ results }}
</pre>
{% if debug %}
<h3>Results</h3>
<pre>
{{ results }}
</pre>
<h3>Latest Segment Metadata</h3>
<pre>
<h3>Latest Segment Metadata</h3>
<pre>
{{ latest_metadata }}
</pre>
{% endif %}
</div>
</pre>
{% endif %}
</div>
</div>
{% endblock %}
{% block tail %}
{{ super() }}
<script>
$( document ).ready(function() {
//`:$(".select2").select2();
// $(".select2_tags").select2({tags: true});
});
</script>
{% endblock %}

View File

@ -0,0 +1,5 @@
{% extends "panoramix/datasource.html" %}
{% block viz %}
<span class="alert alert-danger">No data: review your incantations.</span>
{% endblock %}

View File

@ -0,0 +1,56 @@
{% extends "bootstrap/base.html" %}
{% block title %}Panoramix - A Druid UI{% endblock %}
{% block html_attribs %} lang="en"{% endblock %}
{% block head %}
{{super()}}
<link rel="icon" type="image/png" href="{{url_for('.static', filename='chaudron.png')}}">
{% endblock %}
{% block styles %}
{{super()}}
<link rel="stylesheet" href="{{url_for('.static', filename='bootstrap-theme.css')}}">
<link rel="stylesheet" href="{{url_for('.static', filename='main.css')}}">
<link rel="stylesheet" href="{{url_for('.static', filename='select2.min.css')}}">
<link rel="stylesheet" href="{{url_for('.static', filename='select2-bootstrap.css')}}">
{% endblock %}
{% block navbar %}
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#" class="pull-left">
Panoramix
</a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active"><a href="#">Link <span class="sr-only">(current)</span></a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="#">Action</a></li>
<li><a href="#">Another action</a></li>
<li><a href="#">Something else here</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">Separated link</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">One more separated link</a></li>
</ul>
</li>
</ul>
<ul class="nav navbar-nav navbar-right"></ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
{% endblock %}

View File

@ -3,7 +3,7 @@
<div id="chart"></div>
{% endblock %}
{% block scripts %}
{% block tail %}
{{ super() }}
<script src="{{ url_for("static", filename="highcharts.js") }}"></script>
<script>

View File

@ -5,4 +5,7 @@
{% block scripts %}
{{ super() }}
<script>
$('table').css('background-color', 'white');
</script>
{% endblock %}

View File

@ -1,9 +1,11 @@
from pydruid import client
from pydruid.utils.filters import Dimension, Filter
from datetime import datetime
from flask import render_template, flash
import pandas as pd
from pandas_highcharts.core import serialize
from panoramix import settings
from pydruid.utils import aggregators as agg
from collections import OrderedDict
CHART_ARGS = {
@ -12,33 +14,24 @@ CHART_ARGS = {
'render_to': 'chart',
}
# temp hack
metric = "count"
class BaseViz(object):
verbose_name = "Base Viz"
template = "panoramix/datasource.html"
def __init__(self, datasource, form_class, form_data):
def __init__(self, datasource, form_class, form_data, admin_view):
self.datasource = datasource
self.form_class = form_class
self.form_data = form_data
self.metric = form_data.get('metric')
self.admin_view = admin_view
self.df = self.bake_query()
if self.df is not None:
self.df.timestamp = pd.to_datetime(self.df.timestamp)
self.df_prep()
self.form_prep()
def bake_query(self):
ds = self.datasource
def query_filters(self):
args = self.form_data
groupby = args.getlist("groupby") or []
granularity = args.get("granularity")
metric = "count"
limit = int(args.get("limit", ROW_LIMIT)) or ROW_LIMIT
since = args.get("since", "all")
from_dttm = (datetime.now() - since_l[since]).isoformat()
# Building filters
i = 1
filters = None
@ -57,8 +50,7 @@ class BaseViz(object):
for s in eq.split(','):
s = s.strip()
fields.append(Filter.build_filter(Dimension(col)==s))
cond = Filter(type="or", fields=fields)
cond = Filter(type="and", fields=fields)
if filters:
filters = cond and filters
@ -67,29 +59,42 @@ class BaseViz(object):
else:
break
i += 1
return filters
kw = {}
if filters:
kw['filter'] = filters
query.groupby(
datasource=ds.name,
granularity=granularity or 'all',
intervals=from_dttm + '/' + datetime.now().isoformat(),
dimensions=groupby,
aggregations={"count": client.doublesum(metric)},
#filter=filters,
limit_spec={
def query_obj(self):
ds = self.datasource
args = self.form_data
groupby = args.getlist("groupby") or []
granularity = args.get("granularity")
metric = "count"
limit = int(
args.get("limit", settings.ROW_LIMIT)) or settings.ROW_LIMIT
since = args.get("since", "all")
from_dttm = (datetime.now() - settings.since_l[since]).isoformat()
d = {
'datasource': ds.name,
'granularity': granularity or 'all',
'intervals': from_dttm + '/' + datetime.now().isoformat(),
'dimensions': groupby,
'aggregations': {"count": agg.doublesum(metric)},
'limit_spec': {
"type": "default",
"limit": limit,
"columns": [{
"dimension" : metric,
"direction" : "descending",
},],
"dimension": metric,
"direction": "descending",
}],
},
**kw
)
return query.export_pandas()
}
filters = self.query_filters()
if filters:
d['filter'] = filters
return d
def bake_query(self):
client = settings.get_pydruid_client()
client.groupby(**self.query_obj())
return client.export_pandas()
def df_prep(self, ):
pass
@ -97,17 +102,21 @@ class BaseViz(object):
def form_prep(self):
pass
def render_no_data(self):
self.template = "panoramix/no_data.html"
return BaseViz.render(self)
def render(self, *args, **kwargs):
form = self.form_class(self.form_data)
return render_template(
self.template, form=form)
return self.admin_view.render(
self.template, form=form, viz=self, datasource=self.datasource,
*args, **kwargs)
class TableViz(BaseViz):
verbose_name = "Table View"
template = 'panoramix/viz_table.html'
def render(self):
form = self.form_class(self.form_data)
if self.df is None or self.df.empty:
flash("No data.", "error")
table = None
@ -115,53 +124,85 @@ class TableViz(BaseViz):
if self.form_data.get("granularity") == "all":
del self.df['timestamp']
table = self.df.to_html(
classes=["table", "table-striped", 'table-bordered'],
classes=[
'table', 'table-striped', 'table-bordered',
'table-condensed'],
index=False)
return render_template(
self.template, form=form, table=table)
return super(TableViz, self).render(table=table)
class HighchartsViz(BaseViz):
verbose_name = "Base Highcharts Viz"
template = 'panoramix/viz_highcharts.html'
chart_kind = 'line'
def render(self, *args, **kwargs):
form = self.form_class(self.form_data)
if self.df is None or self.df.empty:
flash("No data.", "error")
else:
table = self.df.to_html(
classes=["table", "table-striped", 'table-bordered'],
index=False)
return render_template(
self.template, form=form, table=table,
*args, **kwargs)
class TimeSeriesViz(HighchartsViz):
verbose_name = "Time Series - Line Chart"
chart_kind = "line"
def render(self):
metric = self.metric
df = self.df
df = df.pivot_table(
index="timestamp",
columns=[
col for col in df.columns if col not in ["timestamp", metric]],
values=[metric])
chart_js = serialize(
df, kind=self.chart_kind, **CHART_ARGS)
chart_js = serialize(df, kind=self.chart_kind, **CHART_ARGS)
return super(TimeSeriesViz, self).render(chart_js=chart_js)
def bake_query(self):
"""
Doing a 2 phase query where we limit the number of series.
"""
client = settings.get_pydruid_client()
qry = self.query_obj()
qry['granularity'] = "all"
client.groupby(**qry)
df = client.export_pandas()
dims = qry['dimensions']
filters = []
for index, row in df.iterrows():
fields = []
for dim in dims:
f = Filter.build_filter(Dimension(dim) == row[dim])
fields.append(f)
if len(fields) > 1:
filters.append(Filter.build_filter(Filter(type="and", fields=fields)))
elif fields:
filters.append(fields[0])
qry = self.query_obj()
if filters:
ff = Filter(type="or", fields=filters)
qry['filter'] = ff
del qry['limit_spec']
client.groupby(**qry)
return client.export_pandas()
class TimeSeriesAreaViz(TimeSeriesViz):
verbose_name = "Time Series - Area Chart"
chart_kind = "area"
class TimeSeriesBarViz(TimeSeriesViz):
verbose_name = "Time Series - Bar Chart"
chart_kind = "bar"
class DistributionBarViz(HighchartsViz):
verbose_name = "Distribution - Bar Chart"
chart_kind = "bar"
def query_obj(self):
d = super(DistributionBarViz, self).query_obj()
d['granularity'] = "all"
return d
def render(self):
metric = self.metric
df = self.df
df = df.pivot_table(
index=[
@ -172,9 +213,33 @@ class DistributionBarViz(HighchartsViz):
df, kind=self.chart_kind, **CHART_ARGS)
return super(DistributionBarViz, self).render(chart_js=chart_js)
viz_types = {
'table': TableViz,
'line': TimeSeriesViz,
'area': TimeSeriesAreaViz,
'dist_bar': DistributionBarViz,
}
class DistributionPieViz(HighchartsViz):
verbose_name = "Distribution - Pie Chart"
chart_kind = "pie"
def query_obj(self):
d = super(DistributionPieViz, self).query_obj()
d['granularity'] = "all"
return d
def render(self):
metric = self.metric
df = self.df
df = df.pivot_table(
index=[
col for col in df.columns if col not in ['timestamp', metric]],
values=[metric])
df = df.sort(metric, ascending=False)
chart_js = serialize(
df, kind=self.chart_kind, **CHART_ARGS)
return super(DistributionPieViz, self).render(chart_js=chart_js)
viz_types = OrderedDict([
['table', TableViz],
['line', TimeSeriesViz],
['area', TimeSeriesAreaViz],
['bar', TimeSeriesBarViz],
['dist_bar', DistributionBarViz],
['pie', DistributionPieViz],
])

View File

@ -1,7 +1,10 @@
flask
flask-admin
flask-bootstrap
flask-sqlalchemy
pandas
pandas-highcharts
pydruid
python-dateutil
requests
wtforms