Navigation Menu

Skip to content

Commit

Permalink
Reactify dashboard grid (#523)
Browse files Browse the repository at this point in the history
* Use react-grid-layout instead of gridster

* visualizations show and resize

* display slice name and description; links work

* positioning of widgets to match gridster, rowHeight matches

* Change margins, rowHeight, unpositioned viz, and expandedSlices to match gridster

* Saving dashboard, deleting slices, formatting on slices (chart control and resize handle), expanded slices fixed.

* responsiveness + use es6 classes

* Minor ui fixes + linting

* CSS transforms on slices messes up nvd3 tooltip positioning.
Turn off CSS transforms for the time being, with a cost of painting speed.

Issue is currently being looked at on the nvd3 repo
PR: novus/nvd3#1674

* Remove breakpoint listener, fires when it shouldn't (i.e. too often)

* resize is no longer buggy, minor cleanup

* gridster class, const, landscape error

* one source of data for data to front end from python
  • Loading branch information
georgeke authored and mistercrunch committed Jun 2, 2016
1 parent fe6628b commit c78d368
Show file tree
Hide file tree
Showing 10 changed files with 219 additions and 118 deletions.
Expand Up @@ -4,18 +4,193 @@ var px = require('./modules/caravel.js');
var d3 = require('d3');
var showModal = require('./modules/utils.js').showModal;
require('bootstrap');
import React from 'react';
import { render } from 'react-dom';

var ace = require('brace');
require('brace/mode/css');
require('brace/theme/crimson_editor');

require('./caravel-select2.js');
require('../node_modules/gridster/dist/jquery.gridster.min.css');
require('../node_modules/gridster/dist/jquery.gridster.min.js');
require('../node_modules/react-grid-layout/css/styles.css');
require('../node_modules/react-resizable/css/styles.css');

require('../stylesheets/dashboard.css');

import { Responsive, WidthProvider } from "react-grid-layout";
const ResponsiveReactGridLayout = WidthProvider(Responsive);

class SliceCell extends React.Component {
render() {
const slice = this.props.slice,
createMarkup = function () {
return { __html: slice.description_markeddown };
};

return (
<div>
<div className="chart-header">
<div className="row">
<div className="col-md-12 text-center header">
{slice.slice_name}
</div>
<div className="col-md-12 chart-controls">
<div className="pull-left">
<a title="Move chart" data-toggle="tooltip">
<i className="fa fa-arrows drag"/>
</a>
<a className="refresh" title="Force refresh data" data-toggle="tooltip">
<i className="fa fa-repeat"/>
</a>
</div>
<div className="pull-right">
{slice.description ?
<a title="Toggle chart description">
<i className="fa fa-info-circle slice_info" title={slice.description} data-toggle="tooltip"/>
</a>
: ""}
<a href={slice.edit_url} title="Edit chart" data-toggle="tooltip">
<i className="fa fa-pencil"/>
</a>
<a href={slice.slice_url} title="Explore chart" data-toggle="tooltip">
<i className="fa fa-share"/>
</a>
<a className="remove-chart" title="Remove chart from dashboard" data-toggle="tooltip">
<i className="fa fa-close" onClick={this.props.removeSlice.bind(null, slice.slice_id)}/>
</a>
</div>
</div>

</div>
</div>
<div
className="slice_description bs-callout bs-callout-default"
style={this.props.expandedSlices && this.props.expandedSlices[String(slice.slice_id)] ? {} : { display: "none" }}
dangerouslySetInnerHTML={createMarkup()}>
</div>
<div className="row chart-container">
<input type="hidden" value="false"/>
<div id={slice.token} className="token col-md-12">
<img src={"/static/assets/images/loading.gif"} className="loading" alt="loading"/>
<div className="slice_container" id={slice.token + "_con"}></div>
</div>
</div>
</div>
);
}
}

class GridLayout extends React.Component {
removeSlice(sliceId) {
$('[data-toggle="tooltip"]').tooltip("hide");
this.setState({
layout: this.state.layout.filter(function (reactPos) {
return reactPos.i !== String(sliceId);
}),
slices: this.state.slices.filter(function (slice) {
return slice.slice_id !== sliceId;
}),
sliceElements: this.state.sliceElements.filter(function (sliceElement) {
return sliceElement.key !== String(sliceId);
})
});
}

onResizeStop(layout, oldItem, newItem) {
if (oldItem.w != newItem.w || oldItem.h != newItem.h) {
this.setState({
layout: layout
}, function () {
this.props.dashboard.getSlice(newItem.i).resize();
});
}
}

onDragStop(layout) {
this.setState({
layout: layout
});
}

serialize() {
return this.state.layout.map(function (reactPos) {
return {
slice_id: reactPos.i,
col: reactPos.x + 1,
row: reactPos.y,
size_x: reactPos.w,
size_y: reactPos.h
};
});
}

componentWillMount() {
var layout = [],
sliceElements = [];

this.props.slices.forEach(function (slice, index) {
var pos = this.props.posDict[slice.slice_id];
if (!pos) {
pos = {
col: (index * 4 + 1) % 12,
row: Math.floor((index) / 3) * 4,
size_x: 4,
size_y: 4
};
}

sliceElements.push(
<div
id={"slice_" + slice.slice_id}
key={slice.slice_id}
data-slice-id={slice.slice_id}
className={"widget " + slice.viz_name}>
<SliceCell
slice={slice}
removeSlice={this.removeSlice.bind(this)}
expandedSlices={this.props.dashboard.metadata.expanded_slices}/>
</div>
);

layout.push({
i: String(slice.slice_id),
x: pos.col - 1,
y: pos.row,
w: pos.size_x,
minW: 2,
h: pos.size_y
});
}, this);

this.setState({
layout: layout,
sliceElements: sliceElements,
slices: this.props.slices
});
}

render() {
return (
<ResponsiveReactGridLayout
className="layout"
layouts={{ lg: this.state.layout }}
onResizeStop={this.onResizeStop.bind(this)}
onDragStop={this.onDragStop.bind(this)}
cols={{ lg: 12, md: 12, sm: 10, xs: 8, xxs: 6 }}
rowHeight={100}
autoSize={true}
margin={[20, 20]}
useCSSTransforms={false}
draggableHandle=".drag">
{this.state.sliceElements}
</ResponsiveReactGridLayout>
);
}
}

var Dashboard = function (dashboardData) {
var reactGridLayout;

var dashboard = $.extend(dashboardData, {
filters: {},
init: function () {
Expand Down Expand Up @@ -128,30 +303,17 @@ var Dashboard = function (dashboardData) {
}
},
initDashboardView: function () {
var posDict = {}
this.position_json.forEach(function (position) {
posDict[position.slice_id] = position;
});

reactGridLayout = render(
<GridLayout slices={this.slices} posDict={posDict} dashboard={dashboard}/>,
document.getElementById("grid-container")
);

dashboard = this;
var gridster = $(".gridster ul").gridster({
autogrow_cols: true,
widget_margins: [10, 10],
widget_base_dimensions: [95, 95],
draggable: {
handle: '.drag'
},
resize: {
enabled: true,
stop: function (e, ui, element) {
dashboard.getSlice($(element).attr('slice_id')).resize();
}
},
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');

// Displaying widget controls on hover
$('.chart-header').hover(
Expand All @@ -162,18 +324,18 @@ var Dashboard = function (dashboardData) {
$(this).find('.chart-controls').fadeOut(300);
}
);
$("div.gridster").css('visibility', 'visible');
$("div.grid-container").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;
expanded_slices[$(widget).attr('data-slice-id')] = true;
}
});
var data = {
positions: gridster.serialize(),
positions: reactGridLayout.serialize(),
css: editor.getValue(),
expanded_slices: expanded_slices
};
Expand Down Expand Up @@ -236,12 +398,8 @@ var Dashboard = function (dashboardData) {
slice.render(true);
});
});
$("a.remove-chart").click(function () {
var li = $(this).parents("li");
gridster.remove_widget(li);
});

$("li.widget").click(function (e) {
$("div.widget").click(function (e) {
var $this = $(this);
var $target = $(e.target);

Expand Down
2 changes: 2 additions & 0 deletions caravel/assets/package.json
Expand Up @@ -65,6 +65,8 @@
"react": "^0.14.7",
"react-bootstrap": "^0.28.3",
"react-dom": "^0.14.7",
"react-grid-layout": "^0.12.3",
"react-resizable": "^1.3.3",
"select2": "3.5",
"select2-bootstrap-css": "^1.4.6",
"style-loader": "^0.13.0",
Expand Down
12 changes: 7 additions & 5 deletions caravel/assets/stylesheets/caravel.css
Expand Up @@ -170,29 +170,31 @@ li.widget:hover {
z-index: 1000;
}

li.widget .chart-header {
div.widget .chart-header {
padding: 5px;
background-color: #f1f1f1;
}

li.widget .chart-header a {
div.widget .chart-header a {
margin-left: 5px;
}

#is_cached {
display: none;
}

li.widget .chart-controls {
div.widget .chart-controls {
background-clip: content-box;
background-color: #f1f1f1;
position: absolute;
right: 0;
left: 0;
padding: 0px 5px;
top: 5px;
padding: 5px 5px;
opacity: 0.75;
display: none;
}

li.widget .slice_container {
div.widget .slice_container {
overflow: auto;
}
11 changes: 5 additions & 6 deletions caravel/assets/stylesheets/dashboard.css
Expand Up @@ -4,23 +4,22 @@
.dashboard i.drag {
cursor: move !important;
}
.dashboard .gridster .preview-holder {
.dashboard .slice-grid .preview-holder {
z-index: 1;
position: absolute;
background-color: #AAA;
border-color: #AAA;
opacity: 0.3;
}
.gridster li.widget{
list-style-type: none;

.slice-grid div.widget{
border-radius: 0;
margin: 5px;
border: 1px solid #ccc;
box-shadow: 2px 1px 5px -2px #aaa;
background-color: #fff;
}
.dashboard .gridster .dragging,
.dashboard .gridster .resizing {
.dashboard .slice-grid .dragging,
.dashboard .slice-grid .resizing {
opacity: 0.5;
}
.dashboard img.loading {
Expand Down
2 changes: 1 addition & 1 deletion caravel/assets/visualizations/pivot_table.css
@@ -1,4 +1,4 @@
.gridster .widget.pivot_table .slice_container {
.slice-grid .widget.pivot_table .slice_container {
overflow: auto !important;
}

Expand Down
2 changes: 1 addition & 1 deletion caravel/assets/visualizations/table.css
@@ -1,4 +1,4 @@
.gridster .widget.table .slice_container {
.slice-grid .widget.table .slice_container {
overflow: auto !important;
}

Expand Down
2 changes: 1 addition & 1 deletion caravel/assets/webpack.config.js
Expand Up @@ -6,7 +6,7 @@ var config = {
// for now generate one compiled js file per entry point / html page
entry: {
'css-theme': APP_DIR + '/javascripts/css-theme.js',
dashboard: APP_DIR + '/javascripts/dashboard.js',
dashboard: APP_DIR + '/javascripts/dashboard.jsx',
explore: APP_DIR + '/javascripts/explore.js',
welcome: APP_DIR + '/javascripts/welcome.js',
sql: APP_DIR + '/javascripts/sql.js',
Expand Down
7 changes: 7 additions & 0 deletions caravel/models.py
Expand Up @@ -197,6 +197,7 @@ def datasource_id(self):

@property
def data(self):
"""Data used to render slice in templates"""
d = {}
self.token = ''
try:
Expand All @@ -205,6 +206,11 @@ def data(self):
except Exception as e:
d['error'] = str(e)
d['slice_id'] = self.id
d['slice_name'] = self.slice_name
d['description'] = self.description
d['slice_url'] = self.slice_url
d['edit_url'] = self.edit_url
d['description_markeddown'] = self.description_markeddown
return d

@property
Expand Down Expand Up @@ -309,6 +315,7 @@ def json_data(self):
'dashboard_title': self.dashboard_title,
'slug': self.slug,
'slices': [slc.data for slc in self.slices],
'position_json': json.loads(self.position_json),
}
return json.dumps(d)

Expand Down

0 comments on commit c78d368

Please sign in to comment.