mirror of https://github.com/apache/superset.git
Reactify dashboard grid (#523)
* 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: https://github.com/novus/nvd3/pull/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
This commit is contained in:
parent
fe6628b0a4
commit
c78d3682ac
|
@ -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 () {
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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);
|
||||
|
|
@ -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",
|
||||
|
|
|
@ -170,12 +170,12 @@ 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;
|
||||
}
|
||||
|
||||
|
@ -183,16 +183,18 @@ li.widget .chart-header a {
|
|||
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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.gridster .widget.pivot_table .slice_container {
|
||||
.slice-grid .widget.pivot_table .slice_container {
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.gridster .widget.table .slice_container {
|
||||
.slice-grid .widget.table .slice_container {
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -197,6 +197,7 @@ class Slice(Model, AuditMixinNullable):
|
|||
|
||||
@property
|
||||
def data(self):
|
||||
"""Data used to render slice in templates"""
|
||||
d = {}
|
||||
self.token = ''
|
||||
try:
|
||||
|
@ -205,6 +206,11 @@ class Slice(Model, AuditMixinNullable):
|
|||
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
|
||||
|
@ -309,6 +315,7 @@ class Dashboard(Model, AuditMixinNullable):
|
|||
'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)
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
{% endblock %}
|
||||
{% block title %}[dashboard] {{ dashboard.dashboard_title }}{% endblock %}
|
||||
{% block body %}
|
||||
|
||||
<div class="dashboard container-fluid" data-dashboard="{{ dashboard.json_data }}" data-css="{{ dashboard.css }}">
|
||||
|
||||
<!-- Modal -->
|
||||
|
@ -97,71 +98,9 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gridster content_fluid" style="visibility: hidden;">
|
||||
<ul>
|
||||
{% for slice in dashboard.slices %}
|
||||
{% set pos = pos_dict.get(slice.id, {}) %}
|
||||
|
||||
|
||||
<li
|
||||
id="slice_{{ slice.id }}"
|
||||
slice_id="{{ slice.id }}"
|
||||
class="widget {{ slice.viz_type }}"
|
||||
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="chart-header">
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center header">
|
||||
{{ slice.slice_name }}
|
||||
</div>
|
||||
<div class="col-md-12 chart-controls">
|
||||
<div class="pull-left">
|
||||
<a title="Move chart" data-toggle="tooltip">
|
||||
<i class="fa fa-arrows drag"></i>
|
||||
</a>
|
||||
<a class="refresh" title="Force refresh data" data-toggle="tooltip">
|
||||
<i class="fa fa-repeat"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if slice.description %}
|
||||
<a title="Toggle chart description">
|
||||
<i class="fa fa-info-circle slice_info" slice_id="{{ slice.id }}" title="{{ slice.description }}" data-toggle="tooltip"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ slice.edit_url }}" title="Edit chart" data-toggle="tooltip">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</a>
|
||||
<a href="{{ slice.slice_url }}" title="Explore chart" data-toggle="tooltip">
|
||||
<i class="fa fa-share"></i>
|
||||
</a>
|
||||
<a class="remove-chart" title="Remove chart from dashboard" data-toggle="tooltip">
|
||||
<i class="fa fa-close"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<div class="row chart-container">
|
||||
<input type="hidden" slice_id="{{ slice.id }}" value="false">
|
||||
<div id="{{ slice.token }}" class="token col-md-12">
|
||||
<img src="{{ url_for("static", filename="assets/images/loading.gif") }}" class="loading" alt="loading">
|
||||
<div class="slice_container" id="{{ slice.token }}_con"></div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<!-- gridster class used for backwards compatibility -->
|
||||
<div id="grid-container" class="slice-grid gridster">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -883,15 +883,9 @@ class Caravel(BaseView):
|
|||
pass
|
||||
dashboard(dashboard_id=dash.id)
|
||||
|
||||
pos_dict = {}
|
||||
if dash.position_json:
|
||||
pos_dict = {
|
||||
int(o['slice_id']): o
|
||||
for o in json.loads(dash.position_json)}
|
||||
return self.render_template(
|
||||
"caravel/dashboard.html", dashboard=dash,
|
||||
templates=templates,
|
||||
pos_dict=pos_dict,
|
||||
dash_save_perm=appbuilder.sm.has_access('can_save_dash', 'Caravel'),
|
||||
dash_edit_perm=appbuilder.sm.has_access('can_edit', 'DashboardModelView'))
|
||||
|
||||
|
|
Loading…
Reference in New Issue