From c78d3682ac6a6f155e0aa239175290dd15ee11a0 Mon Sep 17 00:00:00 2001 From: George Ke Date: Thu, 2 Jun 2016 12:31:05 -0700 Subject: [PATCH] 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 --- .../{dashboard.js => dashboard.jsx} | 224 +++++++++++++++--- caravel/assets/package.json | 2 + caravel/assets/stylesheets/caravel.css | 12 +- caravel/assets/stylesheets/dashboard.css | 11 +- caravel/assets/visualizations/pivot_table.css | 2 +- caravel/assets/visualizations/table.css | 2 +- caravel/assets/webpack.config.js | 2 +- caravel/models.py | 7 + caravel/templates/caravel/dashboard.html | 69 +----- caravel/views.py | 6 - 10 files changed, 219 insertions(+), 118 deletions(-) rename caravel/assets/javascripts/{dashboard.js => dashboard.jsx} (58%) diff --git a/caravel/assets/javascripts/dashboard.js b/caravel/assets/javascripts/dashboard.jsx similarity index 58% rename from caravel/assets/javascripts/dashboard.js rename to caravel/assets/javascripts/dashboard.jsx index 89b96f324d1d5..b28d31c591c7c 100644 --- a/caravel/assets/javascripts/dashboard.js +++ b/caravel/assets/javascripts/dashboard.jsx @@ -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 ( +
+
+
+
+ {slice.slice_name} +
+
+ +
+ {slice.description ? + + + + : ""} + + + + + + + + + +
+
+ +
+
+
+
+
+ +
+ loading +
+
+
+
+ ); + } +} + +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( +
+ +
+ ); + + 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 ( + + {this.state.sliceElements} + + ); + } +} + 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( + , + 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); diff --git a/caravel/assets/package.json b/caravel/assets/package.json index e6f5e0188cfd6..07a41fa154708 100644 --- a/caravel/assets/package.json +++ b/caravel/assets/package.json @@ -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", diff --git a/caravel/assets/stylesheets/caravel.css b/caravel/assets/stylesheets/caravel.css index 05614ecd6365d..b30d868f1a757 100644 --- a/caravel/assets/stylesheets/caravel.css +++ b/caravel/assets/stylesheets/caravel.css @@ -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; } diff --git a/caravel/assets/stylesheets/dashboard.css b/caravel/assets/stylesheets/dashboard.css index e9376487fd547..3a77cf0c71c96 100644 --- a/caravel/assets/stylesheets/dashboard.css +++ b/caravel/assets/stylesheets/dashboard.css @@ -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 { diff --git a/caravel/assets/visualizations/pivot_table.css b/caravel/assets/visualizations/pivot_table.css index 8b8b136d3c453..7d94f85344af4 100644 --- a/caravel/assets/visualizations/pivot_table.css +++ b/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; } diff --git a/caravel/assets/visualizations/table.css b/caravel/assets/visualizations/table.css index 3d11841bb4da5..655a360c14c40 100644 --- a/caravel/assets/visualizations/table.css +++ b/caravel/assets/visualizations/table.css @@ -1,4 +1,4 @@ -.gridster .widget.table .slice_container { +.slice-grid .widget.table .slice_container { overflow: auto !important; } diff --git a/caravel/assets/webpack.config.js b/caravel/assets/webpack.config.js index d9253f3d5c413..cabb8bc12ceb2 100644 --- a/caravel/assets/webpack.config.js +++ b/caravel/assets/webpack.config.js @@ -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', diff --git a/caravel/models.py b/caravel/models.py index 0a91ee8f78d49..bdfb0e80bca3d 100644 --- a/caravel/models.py +++ b/caravel/models.py @@ -197,6 +197,7 @@ def datasource_id(self): @property def data(self): + """Data used to render slice in templates""" d = {} self.token = '' try: @@ -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 @@ -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) diff --git a/caravel/templates/caravel/dashboard.html b/caravel/templates/caravel/dashboard.html index 5c8cbed67fb5b..bde25e2031add 100644 --- a/caravel/templates/caravel/dashboard.html +++ b/caravel/templates/caravel/dashboard.html @@ -6,6 +6,7 @@ {% endblock %} {% block title %}[dashboard] {{ dashboard.dashboard_title }}{% endblock %} {% block body %} +
@@ -97,71 +98,9 @@

- {% endblock %} diff --git a/caravel/views.py b/caravel/views.py index b3bb3b4ec76ae..dd351d066fe02 100644 --- a/caravel/views.py +++ b/caravel/views.py @@ -883,15 +883,9 @@ def dashboard(**kwargs): # noqa 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'))