Merge pull request #120 from mistercrunch/sliceinfo

Slice information can be displayed in dashboard
This commit is contained in:
Maxime Beauchemin 2016-01-19 00:37:48 -08:00
commit ef612ed66c
12 changed files with 152 additions and 33 deletions

View File

@ -20,6 +20,7 @@ List of TODO items for Panoramix
An example of a layer might be "holidays" or "site outages", ...
* **Worth doing? User defined groups:** People could define mappings in the UI of say "Countries I follow" and apply it to different datasets. For now, this is done by writing CASE-WHEN-type expression which is probably good enough.
## Easy-ish fix
* datasource in explore mode could be a dropdown
* Create a set of slices and dashboard on top of the World Bank dataset that ship with load_examples

View File

@ -0,0 +1,21 @@
"""empty message
Revision ID: 43df8de3a5f4
Revises: 7dbf98566af7
Create Date: 2016-01-18 23:43:16.073483
"""
# revision identifiers, used by Alembic.
revision = '43df8de3a5f4'
down_revision = '7dbf98566af7'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
def upgrade():
op.add_column('dashboards', sa.Column('json_metadata', sa.Text(), nullable=True))
def downgrade():
op.drop_column('dashboards', 'json_metadata')

View File

@ -0,0 +1,21 @@
"""empty message
Revision ID: 7dbf98566af7
Revises: 8e80a26a31db
Create Date: 2016-01-17 22:00:23.640788
"""
# revision identifiers, used by Alembic.
revision = '7dbf98566af7'
down_revision = '8e80a26a31db'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
def upgrade():
op.add_column('slices', sa.Column('description', sa.Text(), nullable=True))
def downgrade():
op.drop_column('slices', 'description')

View File

@ -66,6 +66,7 @@ class Slice(Model, AuditMixinNullable):
datasource_name = Column(String(2000))
viz_type = Column(String(250))
params = Column(Text)
description = Column(Text)
table = relationship(
'SqlaTable', foreign_keys=[table_id], backref='slices')
@ -88,6 +89,10 @@ class Slice(Model, AuditMixinNullable):
form_data=d)
return viz
@property
def description_markeddown(self):
return utils.markdown(self.description)
@property
def datasource_id(self):
return self.table_id or self.druid_datasource_id
@ -154,6 +159,7 @@ class Dashboard(Model, AuditMixinNullable):
position_json = Column(Text)
description = Column(Text)
css = Column(Text)
json_metadata = Column(Text)
slug = Column(String(255), unique=True)
slices = relationship(
'Slice', secondary=dashboard_slices, backref='dashboards')
@ -165,6 +171,10 @@ class Dashboard(Model, AuditMixinNullable):
def url(self):
return "/panoramix/dashboard/{}/".format(self.slug or self.id)
@property
def metadata_dejson(self):
return json.loads(self.json_metadata)
def dashboard_link(self):
return '<a href="{self.url}">{self.dashboard_title}</a>'.format(self=self)

View File

@ -11,7 +11,18 @@ html>body{
margin-left: 365px;
}
.slice_description{
padding: 8px;
margin: 5px;
border: 1px solid #DDD;
background-color: #F8F8F8;
border-radius: 5px;
font-size: 12px;
}
.slice_info{
cursor: pointer;
}
.padded{
padding: 10px;
@ -23,7 +34,7 @@ html>body{
}
.slice_container {
height: 100%;
//height: 100%;
}
.container-fluid {
text-align: left;

View File

@ -106,7 +106,13 @@ var px = (function() {
return token.width();
},
height: function(){
return token.height() - 25;
var others = 0;
var widget = container.parents('.widget');
var slice_description = widget.find('.slice_description');
if (slice_description.is(":visible"))
others += widget.find('.slice_description').height() + 25;
others += widget.find('.slice_header').height();
return widget.height() - others;
},
render: function() {
$('.btn-group.results span').attr('disabled','disabled');
@ -416,9 +422,17 @@ var px = (function() {
}).data('gridster');
$("div.gridster").css('visibility', 'visible');
$("#savedash").click(function() {
var expanded_slices = {};
$.each($(".slice_info"), function(i, d){
var widget = $(this).parents('.widget');
var slice_description = widget.find('.slice_description');
if(slice_description.is(":visible"))
expanded_slices[$(d).attr('slice_id')] = true;
});
var data = {
positions: gridster.serialize(),
css: $("#dash_css").val()
css: $("#dash_css").val(),
expanded_slices: expanded_slices,
};
$.ajax({
type: "POST",
@ -432,6 +446,13 @@ var px = (function() {
var li = $(this).parents("li");
gridster.remove_widget(li);
});
$(".slice_info").click(function(){
var widget = $(this).parents('.widget');
var slice_description = widget.find('.slice_description');
slice_description.slideToggle(500, function(){
widget.find('.refresh').click();
});
});
$("table.slice_header").mouseover(function() {
$(this).find("td.icons nobr").show();
});

View File

@ -6,7 +6,7 @@ function viz_nvd3(slice) {
$.getJSON(slice.jsonEndpoint(), function(payload) {
var fd = payload.form_data;
var viz_type = fd.viz_type;
var f = d3.format('.4s');
var f = d3.format('.3s');
nv.addGraph(function() {
if (viz_type === 'line') {
if (fd.show_brush) {
@ -49,6 +49,7 @@ function viz_nvd3(slice) {
} else if (viz_type === 'pie') {
chart = nv.models.pieChart()
chart.showLegend(fd.show_legend);
chart.valueFormat(f);
if (fd.donut) {
chart.donut(true);
chart.donutLabelsOutside(true);
@ -99,8 +100,13 @@ function viz_nvd3(slice) {
chart.showLegend(fd.show_legend);
}
// make space for labels on right
//chart.height($(".chart").height() - 50).margin({"right": 50});
var height = slice.height();
if(chart.hasOwnProperty("x2Axis")) {
height += 30;
}
chart.height(height);
slice.container.css('height', height + 'px');
if ((viz_type === "line" || viz_type === "area") && fd.rich_tooltip) {
chart.useInteractiveGuideline(true);
}
@ -122,7 +128,9 @@ function viz_nvd3(slice) {
else if (fd.x_axis_format !== undefined) {
chart.xAxis.tickFormat(px.timeFormatFactory(fd.x_axis_format));
}
if (fd.contribution || fd.num_period_compare) {
if (chart.yAxis !== undefined)
chart.yAxis.tickFormat(d3.format('.3s'));
if (fd.contribution || fd.num_period_compare || viz_type == 'compare') {
chart.yAxis.tickFormat(d3.format('.3p'));
if (chart.y2Axis != undefined) {
chart.y2Axis.tickFormat(d3.format('.3p'));
@ -140,15 +148,9 @@ function viz_nvd3(slice) {
d3.select(slice.selector).append("svg")
.datum(payload.data)
.transition().duration(500)
.attr('height', height)
.call(chart);
// if it is a two axis chart, rescale it down just a little so it fits in the div.
if(chart.hasOwnProperty("x2Axis")) {
two_axis_chart = $(slice.selector + " > svg");
w = two_axis_chart.width();
h = two_axis_chart.height();
two_axis_chart.get(0).setAttribute('viewBox', '0 0 '+w+' '+(h+30));
}
return chart;
});
slice.done(payload);

View File

@ -79,13 +79,13 @@ px.registerViz('table', function(slice) {
paging: false,
searching: form_data.include_search,
});
slice.container.find('.tooltip').remove();
// Sorting table by main column
if (form_data.metrics.length > 0) {
var main_metric = form_data.metrics[0];
datatable.column(data.columns.indexOf(main_metric)).order( 'desc' ).draw();
}
slice.done(json);
slice.container.parents('.widget').find('.tooltip').remove();
}).fail(function(xhr){
slice.error(xhr.responseText);
});

View File

@ -91,25 +91,35 @@ body {
<nobr class="icons">
<a><i class="fa fa-arrows drag"></i></a>
<a class="refresh"><i class="fa fa-refresh"></i></a>
<a class="bug" data-slice_id="{{ slice.id }}" data-toggle="tooltip" title="console.log(this.slice);"><i class="fa fa-bug"></i></a>
</nobr>
</td>
<td>
<div class="text-center header"><nobr>{{ slice.slice_name }}</nobr></div>
<div class="text-center header">
<nobr>
{{ slice.slice_name }}
{% if slice.description %}
<i class="fa fa-info-circle slice_info" slice_id="{{ slice.id }}"></i>
{% endif %}
</nobr>
</div>
</td>
<td class="icons text-right">
<nobr>
<a href="{{ slice.slice_url }}"><i class="fa fa-play"></i></a>
<a href="{{ slice.edit_url }}"><i class="fa fa-gear"></i></a>
<a href="{{ slice.edit_url }}"><i class="fa fa-edit"></i></a>
<a class="closeslice"><i class="fa fa-close"></i></a>
</br>
</td>
</tr>
</tbody>
</table>
<div id="{{ viz.token }}" class="token" style="height: 100%;">
<div class="slice_description bs-callout bs-callout-default" style="{{ 'display: none;' if "{}".format(slice.id) not in dashboard.metadata_dejson.expanded_slices }}">
{{ slice.description_markeddown | safe }}
</div>
<input type="hidden" slice_id="{{ slice.id }}" value="false">
<div id="{{ viz.token }}" class="token">
<img src="{{ url_for("static", filename="img/loading.gif") }}" class="loading" alt="loading">
<div class="slice_container" id="{{ viz.token }}_con" style="height: 100%; width: 100%;"></div>
<div class="slice_container" id="{{ viz.token }}_con"></div>
</div>
</li>
{% endfor %}
@ -131,9 +141,6 @@ body {
$('#filters').click( function(){
alert(dashboard.readFilters());
});
$('a.bug').click( function(){
console.log(dashboard.getSlice($(this).data('slice_id')));
});
});
</script>
{% endblock %}

View File

@ -42,11 +42,16 @@
</a>
</span>
<span>{{ form.get_field("viz_type")(class_="select2") }}</span>
<span class="alert alert-info" title="Slice Name" data-toggle="tooltip">{{ viz.form_data.slice_name }}
<a class="" href="/slicemodelview/edit/{{ viz.form_data.slice_id }}" data-toggle="tooltip" title="Edit Slice metadata">
<i class="fa fa-edit"></i>
</a>
</span>
{% if slice %}
<span class="alert alert-info" title="Slice" data-toggle="tooltip">{{ slice.slice_name }}
<a class="" href="/slicemodelview/edit/{{ slice.id }}" data-toggle="tooltip" title="Edit Slice metadata">
{% if slice.description %}
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="bottom" title="{{ slice.description }}"></i>
{% endif %}
<i class="fa fa-edit"></i>
</a>
</span>
{% endif %}
<div class="btn-group results pull-right" role="group">
<a role="button" tabindex="0" class="btn btn-default" id="shortner" title="Short URL" data-toggle="popover" data-trigger="focus">
<i class="fa fa-link"></i>
@ -160,7 +165,7 @@
{% include 'appbuilder/flash.html' %}
<div
id="{{ viz.token }}"
class="viz slice {{ viz.viz_type }}"
class="widget viz slice {{ viz.viz_type }}"
data-slice="{{ viz.json_data }}"
style="height: 700px;">
<img src="{{ url_for("static", filename="img/loading.gif") }}" class="loading" alt="loading">

View File

@ -1,6 +1,7 @@
{% if viz.form_data.get("json") == "true" %}
{{ viz.get_json() }}
{% else %}
{% if viz.request.args.get("standalone") == "true" %}
{% extends 'panoramix/standalone.html' %}
{% else %}
@ -22,4 +23,5 @@
<script src="{{ url_for('static', filename=js) }}"></script>
{% endfor %}
{% endblock %}
{% endif %}

View File

@ -205,9 +205,12 @@ class SliceModelView(PanoramixModelView, DeleteMixin):
'slice_link', 'viz_type', 'datasource_type',
'datasource', 'created_by', 'changed_on_']
edit_columns = [
'slice_name', 'viz_type', 'druid_datasource',
'slice_name', 'description', 'viz_type', 'druid_datasource',
'table', 'dashboards', 'params']
base_order = ('changed_on','desc')
description_columns = {
'description': Markup("The content here can be displayed as widget headers in the dashboard view. Supports <a href='https://daringfireball.net/projects/markdown/'>markdown</a>"),
}
appbuilder.add_view(
@ -222,7 +225,8 @@ class DashboardModelView(PanoramixModelView, DeleteMixin):
datamodel = SQLAInterface(models.Dashboard)
list_columns = ['dashboard_link', 'created_by', 'changed_by', 'changed_on_']
edit_columns = [
'dashboard_title', 'slug', 'slices', 'position_json', 'css']
'dashboard_title', 'slug', 'slices', 'position_json', 'css',
'json_metadata']
add_columns = edit_columns
base_order = ('changed_on','desc')
description_columns = {
@ -434,6 +438,15 @@ class Panoramix(BaseView):
payload,
status=status,
mimetype="application/csv")
slice_id = request.args.get("slice_id")
slc = None
if slice_id:
slc = (
db.session.query(models.Slice)
.filter_by(id=request.args.get("slice_id"))
.first()
)
if request.args.get("json") == "true":
status = 200
if config.get("DEBUG"):
@ -451,9 +464,11 @@ class Panoramix(BaseView):
mimetype="application/json")
else:
if config.get("DEBUG"):
resp = self.render_template("panoramix/viz.html", viz=obj)
resp = self.render_template(
"panoramix/viz.html", viz=obj, slice=slc)
try:
resp = self.render_template("panoramix/viz.html", viz=obj)
resp = self.render_template(
"panoramix/viz.html", viz=obj, slice=slc)
except Exception as e:
if config.get("DEBUG"):
raise(e)
@ -490,6 +505,9 @@ class Panoramix(BaseView):
dash = session.query(Dash).filter_by(id=dashboard_id).first()
dash.slices = [o for o in dash.slices if o.id in slice_ids]
dash.position_json = json.dumps(data['positions'], indent=4)
dash.json_metadata = json.dumps({
'expanded_slices': data['expanded_slices'],
}, indent=4)
dash.css = data['css']
session.merge(dash)
session.commit()