From ac63d9391b70123e10f4395579aad00b3da5520e Mon Sep 17 00:00:00 2001 From: George Ke Date: Thu, 7 Jul 2016 14:39:27 -0700 Subject: [PATCH] code review + react-bootstrap-table + modularity --- caravel/assets/javascripts/dashboard.jsx | 629 ------------------ .../javascripts/dashboard/Dashboard.jsx | 336 ++++++++++ .../dashboard/components/GridLayout.jsx | 186 ++++++ .../dashboard/components/Modal.jsx | 42 ++ .../dashboard/components/SliceAdder.jsx | 190 ++++++ caravel/assets/package.json | 1 + caravel/assets/stylesheets/dashboard.css | 22 +- caravel/assets/webpack.config.js | 2 +- caravel/views.py | 2 +- 9 files changed, 763 insertions(+), 647 deletions(-) delete mode 100644 caravel/assets/javascripts/dashboard.jsx create mode 100644 caravel/assets/javascripts/dashboard/Dashboard.jsx create mode 100644 caravel/assets/javascripts/dashboard/components/GridLayout.jsx create mode 100644 caravel/assets/javascripts/dashboard/components/Modal.jsx create mode 100644 caravel/assets/javascripts/dashboard/components/SliceAdder.jsx diff --git a/caravel/assets/javascripts/dashboard.jsx b/caravel/assets/javascripts/dashboard.jsx deleted file mode 100644 index 8b8221b119b9e..0000000000000 --- a/caravel/assets/javascripts/dashboard.jsx +++ /dev/null @@ -1,629 +0,0 @@ -var $ = window.$ = require('jquery'); -var jQuery = window.jQuery = $; -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'; -import update from 'immutability-helper'; - -var ace = require('brace'); -require('brace/mode/css'); -require('brace/theme/crimson_editor'); - -require('./caravel-select2.js'); -require('../node_modules/react-grid-layout/css/styles.css'); -require('../node_modules/react-resizable/css/styles.css'); - -require('datatables.net-bs'); -require('../node_modules/datatables-bootstrap3-plugin/media/css/datatables-bootstrap3.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; - }) - }); - } - - 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 = []; - - 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 - }; - } - - 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, - slices: this.props.slices - }); - } - - render() { - return ( - - {this.state.slices.map(function (slice) { - return ( -
- -
- ); - }, this)} -
- ); - } -} - -class SliceAdder extends React.Component { - constructor(props) { - super(props); - - this.state = { - slices: [] - }; - - this.addSlices = this.addSlices.bind(this); - this.toggleSlice = this.toggleSlice.bind(this); - this.slicesLoaded = false; - } - - addSlices() { - var slices = this.state.slices.filter(function (slice) { - return slice.isSelected; - }); - var sliceIds = []; - - var sliceObjects = slices.map(function (slice) { - var sliceObj = px.Slice(slice.data, this.props.dashboard); - $("#slice_" + slice.data.slice_id).find('a.refresh').click(function () { - sliceObj.render(true); - }); - this.props.dashboard.slices.push(sliceObj); - sliceIds.push(sliceObj.data.slice_id) - return sliceObj; - }, this); - - this.props.dashboard.addSlicesToDashboard(sliceIds); - } - - toggleSlice(sliceIndex) { - this.setState({ - slices: update(this.state.slices, { - [sliceIndex]: { - isSelected: { - $set: !this.state.slices[sliceIndex].isSelected - } - } - }) - }); - } - - componentDidMount() { - var uri = "/sliceaddview/api/read?_flt_0_created_by=" + this.props.dashboard.curUserId; - this.slicesRequest = $.get(uri, function (response) { - this.slicesLoaded = true; - this.setState({ - slices: response.result - }, function () { - $("#add-slice-container table").DataTable(); - }); - }.bind(this)); - } - - componentWillUnmount() { - this.slicesRequest.abort(); - } - - render() { - return ( - - ); - } -} - -var Dashboard = function (dashboardData) { - var dashboard = $.extend(dashboardData, { - filters: {}, - init: function () { - this.initDashboardView(); - this.firstLoad = true; - px.initFavStars(); - var sliceObjects = [], - dash = this; - dashboard.slices.forEach(function (data) { - if (data.error) { - var html = '
' + data.error + '
'; - $("#slice_" + data.slice_id).find('.token').html(html); - } else { - var slice = px.Slice(data, dash); - $("#slice_" + data.slice_id).find('a.refresh').click(function () { - slice.render(true); - }); - sliceObjects.push(slice); - } - }); - this.slices = sliceObjects; - this.refreshTimer = null; - this.startPeriodicRender(0); - this.bindResizeToWindowResize(); - }, - setFilter: function (slice_id, col, vals) { - this.addFilter(slice_id, col, vals, false); - }, - addFilter: function (slice_id, col, vals, merge) { - if (merge === undefined) { - merge = true; - } - if (!(slice_id in this.filters)) { - this.filters[slice_id] = {}; - } - if (!(col in this.filters[slice_id]) || !merge) { - this.filters[slice_id][col] = vals; - } else { - this.filters[slice_id][col] = d3.merge([this.filters[slice_id][col], vals]); - } - this.refreshExcept(slice_id); - }, - readFilters: function () { - // Returns a list of human readable active filters - return JSON.stringify(this.filters, null, 4); - }, - bindResizeToWindowResize: function () { - var resizeTimer; - var dash = this; - $(window).on('resize', function (e) { - clearTimeout(resizeTimer); - resizeTimer = setTimeout(function () { - dash.slices.forEach(function (slice) { - slice.resize(); - }); - }, 500); - }); - }, - stopPeriodicRender: function () { - if (this.refreshTimer) { - clearTimeout(this.refreshTimer); - this.refreshTimer = null; - } - }, - startPeriodicRender: function (interval) { - this.stopPeriodicRender(); - var dash = this; - var maxRandomDelay = Math.min(interval * 0.2, 5000); - var refreshAll = function () { - dash.slices.forEach(function (slice) { - var force = !dash.firstLoad; - setTimeout(function () { - slice.render(force); - }, - //Randomize to prevent all widgets refreshing at the same time - maxRandomDelay * Math.random()); - }); - dash.firstLoad = false; - }; - - var fetchAndRender = function () { - refreshAll(); - if (interval > 0) { - dash.refreshTimer = setTimeout(function () { - fetchAndRender(); - }, interval); - } - }; - fetchAndRender(); - }, - refreshExcept: function (slice_id) { - var immune = this.metadata.filter_immune_slices || []; - this.slices.forEach(function (slice) { - if (slice.data.slice_id !== slice_id && immune.indexOf(slice.data.slice_id) === -1) { - slice.render(); - } - }); - }, - clearFilters: function (slice_id) { - delete this.filters[slice_id]; - this.refreshExcept(slice_id); - }, - removeFilter: function (slice_id, col, vals) { - if (slice_id in this.filters) { - if (col in this.filters[slice_id]) { - var a = []; - this.filters[slice_id][col].forEach(function (v) { - if (vals.indexOf(v) < 0) { - a.push(v); - } - }); - this.filters[slice_id][col] = a; - } - } - this.refreshExcept(slice_id); - }, - getSlice: function (slice_id) { - slice_id = parseInt(slice_id, 10); - for (var i=0; i < this.slices.length; i++) { - if (this.slices[i].data.slice_id === slice_id) { - return this.slices[i]; - } - } - }, - showAddSlice: function () { - var slicesOnDashMap = {}; - this.reactGridLayout.serialize().forEach(function (position) { - slicesOnDashMap[position.slice_id] = true; - }, this); - - render( - , - document.getElementById("add-slice-container") - ); - }, - addSlicesToDashboard: function (sliceIds) { - $.ajax({ - type: "POST", - url: '/caravel/add_slices/' + dashboard.id + '/', - data: { - data: JSON.stringify({slice_ids: sliceIds}) - }, - success: function () { - // Refresh page to allow for slices to re-render - window.location.reload(); - }, - error: function (error) { - var respJSON = error.responseJSON; - var errorMsg = (respJSON && respJSON.message) ? respJSON.message : - error.responseText; - showModal({ - title: "Error", - body: "Sorry, there was an error adding slices to this dashboard:
" + errorMsg - }); - console.warn("Add new slices error", error); - } - }); - }, - saveDashboard: 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[$(widget).attr('data-slice-id')] = true; - } - }); - var data = { - positions: this.reactGridLayout.serialize(), - css: this.editor.getValue(), - expanded_slices: expanded_slices - }; - $.ajax({ - type: "POST", - url: '/caravel/save_dash/' + dashboard.id + '/', - data: { - data: JSON.stringify(data) - }, - success: function () { - showModal({ - title: "Success", - body: "This dashboard was saved successfully." - }); - }, - error: function (error) { - var respJSON = error.responseJSON; - var errorMsg = (respJSON && respJSON.message) ? respJSON.message : - error.responseText; - showModal({ - title: "Error", - body: "Sorry, there was an error saving this dashboard:
" + errorMsg - }); - console.warn("Save dashboard error", error); - } - }); - }, - initDashboardView: function () { - this.posDict = {}; - this.position_json.forEach(function (position) { - this.posDict[position.slice_id] = position; - }, this); - - this.reactGridLayout = render( - , - document.getElementById("grid-container") - ); - - this.curUserId = $('.dashboard').data('user'); - - dashboard = this; - - // Displaying widget controls on hover - $('.chart-header').hover( - function () { - $(this).find('.chart-controls').fadeIn(300); - }, - function () { - $(this).find('.chart-controls').fadeOut(300); - } - ); - $("div.grid-container").css('visibility', 'visible'); - $("#savedash").click(this.saveDashboard.bind(this)); - $("#add-slice").click(this.showAddSlice.bind(this)); - - var editor = ace.edit("dash_css"); - this.editor = editor; - editor.$blockScrolling = Infinity; - - editor.setTheme("ace/theme/crimson_editor"); - editor.setOptions({ - minLines: 16, - maxLines: Infinity, - useWorker: false - }); - editor.getSession().setMode("ace/mode/css"); - - $(".select2").select2({ - dropdownAutoWidth: true - }); - $("#css_template").on("change", function () { - var css = $(this).find('option:selected').data('css'); - editor.setValue(css); - - $('#dash_css').val(css); - injectCss("dashboard-template", css); - - }); - $('#filters').click(function () { - showModal({ - title: " Current Global Filters", - body: "The following global filters are currently applied:
" + dashboard.readFilters() - }); - }); - $("#refresh_dash_interval").on("change", function () { - var interval = $(this).find('option:selected').val() * 1000; - dashboard.startPeriodicRender(interval); - }); - $('#refresh_dash').click(function () { - dashboard.slices.forEach(function (slice) { - slice.render(true); - }); - }); - - $("div.widget").click(function (e) { - var $this = $(this); - var $target = $(e.target); - - if ($target.hasClass("slice_info")) { - $this.find(".slice_description").slideToggle(0, function () { - $this.find('.refresh').click(); - }); - } else if ($target.hasClass("controls-toggle")) { - $this.find(".chart-controls").toggle(); - } - }); - - editor.on("change", function () { - var css = editor.getValue(); - $('#dash_css').val(css); - injectCss("dashboard-template", css); - }); - - var css = $('.dashboard').data('css'); - injectCss("dashboard-template", css); - - // Injects the passed css string into a style sheet with the specified className - // If a stylesheet doesn't exist with the passed className, one will be injected into - function injectCss(className, css) { - - var head = document.head || document.getElementsByTagName('head')[0]; - var style = document.querySelector('.' + className); - - if (!style) { - if (className.split(' ').length > 1) { - throw new Error("This method only supports selections with a single class name."); - } - style = document.createElement('style'); - style.className = className; - style.type = 'text/css'; - head.appendChild(style); - } - - if (style.styleSheet) { - style.styleSheet.cssText = css; - } else { - style.innerHTML = css; - } - } - } - }); - dashboard.init(); - return dashboard; -}; - -$(document).ready(function () { - Dashboard($('.dashboard').data('dashboard')); - $('[data-toggle="tooltip"]').tooltip({ container: 'body' }); -}); diff --git a/caravel/assets/javascripts/dashboard/Dashboard.jsx b/caravel/assets/javascripts/dashboard/Dashboard.jsx new file mode 100644 index 0000000000000..1d2cec105e8ac --- /dev/null +++ b/caravel/assets/javascripts/dashboard/Dashboard.jsx @@ -0,0 +1,336 @@ +var $ = window.$ = require('jquery'); +var jQuery = window.jQuery = $; +var px = require('../modules/caravel.js'); +var d3 = require('d3'); +var showModal = require('../modules/utils.js').showModal; + +import React from 'react'; +import { render } from 'react-dom'; +import SliceAdder from './components/SliceAdder.jsx'; +import GridLayout from './components/GridLayout.jsx'; + +var ace = require('brace'); +require('bootstrap'); +require('brace/mode/css'); +require('brace/theme/crimson_editor'); +require('../caravel-select2.js'); +require('../../stylesheets/dashboard.css'); + +var Dashboard = function (dashboardData) { + var dashboard = $.extend(dashboardData, { + filters: {}, + init: function () { + this.initDashboardView(); + this.firstLoad = true; + px.initFavStars(); + var sliceObjects = [], + dash = this; + dashboard.slices.forEach(function (data) { + if (data.error) { + var html = '
' + data.error + '
'; + $('#slice_' + data.slice_id).find('.token').html(html); + } else { + var slice = px.Slice(data, dash); + $('#slice_' + data.slice_id).find('a.refresh').click(function () { + slice.render(true); + }); + sliceObjects.push(slice); + } + }); + this.slices = sliceObjects; + this.refreshTimer = null; + this.startPeriodicRender(0); + this.bindResizeToWindowResize(); + }, + setFilter: function (slice_id, col, vals) { + this.addFilter(slice_id, col, vals, false); + }, + addFilter: function (slice_id, col, vals, merge) { + if (merge === undefined) { + merge = true; + } + if (!(slice_id in this.filters)) { + this.filters[slice_id] = {}; + } + if (!(col in this.filters[slice_id]) || !merge) { + this.filters[slice_id][col] = vals; + } else { + this.filters[slice_id][col] = d3.merge([this.filters[slice_id][col], vals]); + } + this.refreshExcept(slice_id); + }, + readFilters: function () { + // Returns a list of human readable active filters + return JSON.stringify(this.filters, null, 4); + }, + bindResizeToWindowResize: function () { + var resizeTimer; + var dash = this; + $(window).on('resize', function (e) { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(function () { + dash.slices.forEach(function (slice) { + slice.resize(); + }); + }, 500); + }); + }, + stopPeriodicRender: function () { + if (this.refreshTimer) { + clearTimeout(this.refreshTimer); + this.refreshTimer = null; + } + }, + startPeriodicRender: function (interval) { + this.stopPeriodicRender(); + var dash = this; + var maxRandomDelay = Math.min(interval * 0.2, 5000); + var refreshAll = function () { + dash.slices.forEach(function (slice) { + var force = !dash.firstLoad; + setTimeout(function () { + slice.render(force); + }, + //Randomize to prevent all widgets refreshing at the same time + maxRandomDelay * Math.random()); + }); + dash.firstLoad = false; + }; + + var fetchAndRender = function () { + refreshAll(); + if (interval > 0) { + dash.refreshTimer = setTimeout(function () { + fetchAndRender(); + }, interval); + } + }; + fetchAndRender(); + }, + refreshExcept: function (slice_id) { + var immune = this.metadata.filter_immune_slices || []; + this.slices.forEach(function (slice) { + if (slice.data.slice_id !== slice_id && immune.indexOf(slice.data.slice_id) === -1) { + slice.render(); + } + }); + }, + clearFilters: function (slice_id) { + delete this.filters[slice_id]; + this.refreshExcept(slice_id); + }, + removeFilter: function (slice_id, col, vals) { + if (slice_id in this.filters) { + if (col in this.filters[slice_id]) { + var a = []; + this.filters[slice_id][col].forEach(function (v) { + if (vals.indexOf(v) < 0) { + a.push(v); + } + }); + this.filters[slice_id][col] = a; + } + } + this.refreshExcept(slice_id); + }, + getSlice: function (slice_id) { + slice_id = parseInt(slice_id, 10); + for (var i=0; i < this.slices.length; i++) { + if (this.slices[i].data.slice_id === slice_id) { + return this.slices[i]; + } + } + }, + showAddSlice: function () { + var slicesOnDashMap = {}; + this.reactGridLayout.serialize().forEach(function (position) { + slicesOnDashMap[position.slice_id] = true; + }, this); + + render( + , + document.getElementById("add-slice-container") + ); + }, + getAjaxErrorMsg: function (error) { + var respJSON = error.responseJSON; + return (respJSON && respJSON.message) ? respJSON.message : + error.responseText; + }, + addSlicesToDashboard: function (sliceIds) { + $.ajax({ + type: "POST", + url: '/caravel/add_slices/' + dashboard.id + '/', + data: { + data: JSON.stringify({ slice_ids: sliceIds }) + }, + success: function () { + // Refresh page to allow for slices to re-render + window.location.reload(); + }, + error: function (error) { + var errorMsg = this.getAjaxErrorMsg(error); + showModal({ + title: "Error", + body: "Sorry, there was an error adding slices to this dashboard: " + errorMsg + }); + }.bind(this) + }); + }, + saveDashboard: function () { + var expandedSlices = {}; + $.each($(".slice_info"), function (i, d) { + var widget = $(this).parents('.widget'); + var sliceDescription = widget.find('.slice_description'); + if (sliceDescription.is(":visible")) { + expandedSlices[$(widget).attr('data-slice-id')] = true; + } + }); + var data = { + positions: this.reactGridLayout.serialize(), + css: this.editor.getValue(), + expanded_slices: expandedSlices + }; + $.ajax({ + type: "POST", + url: '/caravel/save_dash/' + dashboard.id + '/', + data: { + data: JSON.stringify(data) + }, + success: function () { + showModal({ + title: "Success", + body: "This dashboard was saved successfully." + }); + }, + error: function (error) { + var errorMsg = this.getAjaxErrorMsg(error); + showModal({ + title: "Error", + body: "Sorry, there was an error saving this dashboard: " + errorMsg + }); + } + }); + }, + initDashboardView: function () { + this.posDict = {}; + this.position_json.forEach(function (position) { + this.posDict[position.slice_id] = position; + }, this); + + this.reactGridLayout = render( + , + document.getElementById("grid-container") + ); + + this.curUserId = $('.dashboard').data('user'); + + dashboard = this; + + // Displaying widget controls on hover + $('.chart-header').hover( + function () { + $(this).find('.chart-controls').fadeIn(300); + }, + function () { + $(this).find('.chart-controls').fadeOut(300); + } + ); + $("div.grid-container").css('visibility', 'visible'); + $("#savedash").click(this.saveDashboard.bind(this)); + $("#add-slice").click(this.showAddSlice.bind(this)); + + var editor = ace.edit("dash_css"); + this.editor = editor; + editor.$blockScrolling = Infinity; + + editor.setTheme("ace/theme/crimson_editor"); + editor.setOptions({ + minLines: 16, + maxLines: Infinity, + useWorker: false + }); + editor.getSession().setMode("ace/mode/css"); + + $(".select2").select2({ + dropdownAutoWidth: true + }); + $("#css_template").on("change", function () { + var css = $(this).find('option:selected').data('css'); + editor.setValue(css); + + $('#dash_css').val(css); + injectCss("dashboard-template", css); + + }); + $('#filters').click(function () { + showModal({ + title: " Current Global Filters", + body: "The following global filters are currently applied:
" + dashboard.readFilters() + }); + }); + $("#refresh_dash_interval").on("change", function () { + var interval = $(this).find('option:selected').val() * 1000; + dashboard.startPeriodicRender(interval); + }); + $('#refresh_dash').click(function () { + dashboard.slices.forEach(function (slice) { + slice.render(true); + }); + }); + + $("div.widget").click(function (e) { + var $this = $(this); + var $target = $(e.target); + + if ($target.hasClass("slice_info")) { + $this.find(".slice_description").slideToggle(0, function () { + $this.find('.refresh').click(); + }); + } else if ($target.hasClass("controls-toggle")) { + $this.find(".chart-controls").toggle(); + } + }); + + editor.on("change", function () { + var css = editor.getValue(); + $('#dash_css').val(css); + injectCss("dashboard-template", css); + }); + + var css = $('.dashboard').data('css'); + injectCss("dashboard-template", css); + + // Injects the passed css string into a style sheet with the specified className + // If a stylesheet doesn't exist with the passed className, one will be injected into + function injectCss(className, css) { + + var head = document.head || document.getElementsByTagName('head')[0]; + var style = document.querySelector('.' + className); + + if (!style) { + if (className.split(' ').length > 1) { + throw new Error("This method only supports selections with a single class name."); + } + style = document.createElement('style'); + style.className = className; + style.type = 'text/css'; + head.appendChild(style); + } + + if (style.styleSheet) { + style.styleSheet.cssText = css; + } else { + style.innerHTML = css; + } + } + } + }); + dashboard.init(); + return dashboard; +}; + +$(document).ready(function () { + Dashboard($('.dashboard').data('dashboard')); + $('[data-toggle="tooltip"]').tooltip({ container: 'body' }); +}); diff --git a/caravel/assets/javascripts/dashboard/components/GridLayout.jsx b/caravel/assets/javascripts/dashboard/components/GridLayout.jsx new file mode 100644 index 0000000000000..d3a798977676a --- /dev/null +++ b/caravel/assets/javascripts/dashboard/components/GridLayout.jsx @@ -0,0 +1,186 @@ +import React, { PropTypes } from 'react'; +import { Responsive, WidthProvider } from 'react-grid-layout'; +const ResponsiveReactGridLayout = WidthProvider(Responsive); + +require('../../../node_modules/react-grid-layout/css/styles.css'); +require('../../../node_modules/react-resizable/css/styles.css'); + +const sliceCellPropTypes = { + slice: PropTypes.object.isRequired, + removeSlice: PropTypes.func.isRequired, + expandedSlices: PropTypes.object +}; + +const gridLayoutPropTypes = { + dashboard: PropTypes.object.isRequired, + slices: PropTypes.arrayOf(PropTypes.object).isRequired, + posDict: PropTypes.object.isRequired +}; + +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; + }) + }); + } + + 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 = []; + + 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 + }; + } + + 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, + slices: this.props.slices + }); + } + + render() { + return ( + + {this.state.slices.map((slice) => { + return ( +
+ +
+ ); + })} +
+ ); + } +} + +SliceCell.propTypes = sliceCellPropTypes; +GridLayout.propTypes = gridLayoutPropTypes; + +export default GridLayout; diff --git a/caravel/assets/javascripts/dashboard/components/Modal.jsx b/caravel/assets/javascripts/dashboard/components/Modal.jsx new file mode 100644 index 0000000000000..daf68ca556b30 --- /dev/null +++ b/caravel/assets/javascripts/dashboard/components/Modal.jsx @@ -0,0 +1,42 @@ +import React, { PropTypes } from 'react'; + +const propTypes = { + modalId: PropTypes.string.isRequired, + title: PropTypes.string, + modalContent: PropTypes.node, + customButtons: PropTypes.node +}; + +class Modal extends React.Component { + render() { + return ( + + ); + } +} + +Modal.propTypes = propTypes; + +export default Modal; diff --git a/caravel/assets/javascripts/dashboard/components/SliceAdder.jsx b/caravel/assets/javascripts/dashboard/components/SliceAdder.jsx new file mode 100644 index 0000000000000..5068c25a06469 --- /dev/null +++ b/caravel/assets/javascripts/dashboard/components/SliceAdder.jsx @@ -0,0 +1,190 @@ +import React, { PropTypes } from 'react'; +import update from 'immutability-helper'; +import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table'; +import Modal from './Modal.jsx'; +require('../../../node_modules/react-bootstrap-table/css/react-bootstrap-table.css'); + +const propTypes = { + dashboard: PropTypes.object.isRequired, + caravel: PropTypes.object.isRequired +}; + +class SliceAdder extends React.Component { + constructor(props) { + super(props); + + this.state = { + slices: [] + }; + + this.addSlices = this.addSlices.bind(this); + this.toggleSlice = this.toggleSlice.bind(this); + this.toggleAllSlices = this.toggleAllSlices.bind(this); + this.slicesLoaded = false; + this.selectRowProp = { + mode: "checkbox", + clickToSelect: true, + onSelect: this.toggleSlice, + onSelectAll: this.toggleAllSlices + }; + this.options = { + defaultSortOrder: "desc", + defaultSortName: "modified", + sizePerPage: 10 + }; + } + + componentDidMount() { + var uri = "/sliceaddview/api/read?_flt_0_created_by=" + this.props.dashboard.curUserId; + this.slicesRequest = $.ajax({ + url: uri, + type: 'GET', + success: function (response) { + this.slicesLoaded = true; + + // Prepare slice data for table + let slices = response.result; + slices.forEach(function (slice) { + slice.id = slice.data.slice_id; + slice.sliceName = slice.data.slice_name; + slice.vizType = slice.viz_type; + slice.modified = slice.modified; + }); + + this.setState({ + slices: slices, + selectionMap: {} + }); + }.bind(this), + error: function (error) { + this.errored = true; + this.setState({ + errorMsg: this.props.dashboard.getAjaxErrorMsg(error) + }); + }.bind(this) + }); + } + + componentWillUnmount() { + this.slicesRequest.abort(); + } + + addSlices() { + var slices = this.state.slices.filter(function (slice) { + return this.state.selectionMap[slice.id]; + }, this); + + slices.forEach(function (slice) { + var sliceObj = this.props.caravel.Slice(slice.data, this.props.dashboard); + $("#slice_" + slice.data.slice_id).find('a.refresh').click(function () { + sliceObj.render(true); + }); + this.props.dashboard.slices.push(sliceObj); + }, this); + + this.props.dashboard.addSlicesToDashboard(Object.keys(this.state.selectionMap)); + } + + toggleSlice(slice) { + this.setState({ + selectionMap: update(this.state.selectionMap, { + [slice.id]: { + $set: !this.state.selectionMap[slice.id] + } + }) + }); + } + + toggleAllSlices(value) { + let updatePayload = {}; + + this.state.slices.forEach(function (slice) { + updatePayload[slice.id] = { + $set: value + }; + }, this); + + this.setState({ + selectionMap: update(this.state.selectionMap, updatePayload) + }); + } + + modifiedDateComparator(a, b, order) { + if (order === 'desc') { + if (a.changed_on > b.changed_on) { + return -1; + } else if (a.changed_on < b.changed_on) { + return 1; + } + return 0; + } + + if (a.changed_on < b.changed_on) { + return -1; + } else if (a.changed_on > b.changed_on) { + return 1; + } + return 0; + } + + render() { + const hideLoad = this.slicesLoaded || this.errored; + const enableAddSlice = this.state.selectionMap && Object.keys(this.state.selectionMap).some(function (key) { + return this.state.selectionMap[key]; + }, this); + const modalContent = ( +
+ {hideLoad +
+ {this.state.errorMsg} +
+
+ + Name + Viz + modified}> + Modified + + +
+
+ ); + const customButtons = [ + + ]; + + return ( + + ); + } +} + +SliceAdder.propTypes = propTypes; + +export default SliceAdder; diff --git a/caravel/assets/package.json b/caravel/assets/package.json index 8f1df15888d54..1f79817671493 100644 --- a/caravel/assets/package.json +++ b/caravel/assets/package.json @@ -70,6 +70,7 @@ "nvd3": "1.8.3", "react": "^15.2.0", "react-bootstrap": "^0.28.3", + "react-bootstrap-table": "^2.3.7", "react-dom": "^0.14.8", "react-grid-layout": "^0.12.3", "react-map-gl": "^1.0.0-beta-10", diff --git a/caravel/assets/stylesheets/dashboard.css b/caravel/assets/stylesheets/dashboard.css index 3ed6c099d53cb..b2beb779f3b43 100644 --- a/caravel/assets/stylesheets/dashboard.css +++ b/caravel/assets/stylesheets/dashboard.css @@ -45,27 +45,17 @@ z-index: 888; /* this lets tool tips go on top of other slices */ } -.add-slice-selected { - background: gray; - color: white; -} - -.add-slice-selected:hover { - background: lightgray !important; - color: white !important; -} - -#add-slice-container .modal-body { - height: inherit; - overflow-y: auto; -} - .modal img.loading { width: 50px; margin: 0; position: relative; } -.hidden { +.react-bs-container-body { + max-height: 400px; + overflow-y: auto; +} + +.hidden, #pageDropDown { display: none; } diff --git a/caravel/assets/webpack.config.js b/caravel/assets/webpack.config.js index ef58a1b9cad39..fd71b302d89e1 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.jsx', + dashboard: APP_DIR + '/javascripts/dashboard/Dashboard.jsx', explore: APP_DIR + '/javascripts/explore/explore.jsx', welcome: APP_DIR + '/javascripts/welcome.js', sql: APP_DIR + '/javascripts/sql.js', diff --git a/caravel/views.py b/caravel/views.py index 0309221c95ef2..f77958dddb294 100755 --- a/caravel/views.py +++ b/caravel/views.py @@ -575,7 +575,7 @@ class SliceAsync(SliceModelView): # noqa class SliceAddView(SliceModelView): # noqa list_columns = [ 'slice_link', 'viz_type', - 'creator', 'modified', 'data'] + 'owners', 'modified', 'data', 'changed_on'] label_columns = { 'icons': ' ', 'slice_link': _('Slice'),