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.
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit c78d368

Please sign in to comment.